Jibbalit commited on
Commit
df097e4
·
1 Parent(s): 05f9220

Configure wave tank surgeon Space for target model

Browse files
Files changed (5) hide show
  1. Dockerfile +36 -0
  2. README.md +4 -4
  3. requirements.txt +4 -0
  4. space_runner.py +83 -0
  5. wave_tank_surgeon.py +888 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
2
+
3
+ # Wave tank surgery on Llama-3.2-11B Vision abliterated model
4
+ RUN apt-get update && apt-get install -y \
5
+ python3.10 python3-pip git \
6
+ && rm -rf /var/lib/apt/lists/*
7
+
8
+ WORKDIR /app
9
+
10
+ RUN mkdir -p /app/.cache/huggingface && chown -R 1000:1000 /app/.cache
11
+
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY wave_tank_surgeon.py .
16
+ COPY space_runner.py .
17
+
18
+ ENV HF_TOKEN=${HF_TOKEN}
19
+ ENV HOME=/app
20
+ ENV HF_HOME=/app/.cache/huggingface
21
+ ENV HUGGINGFACE_HUB_CACHE=/app/.cache/huggingface/hub
22
+ ENV TRANSFORMERS_CACHE=/app/.cache/huggingface/transformers
23
+ ENV TORCH_CUDA_ARCH_LIST="80;89;90"
24
+
25
+ EXPOSE 7860
26
+ ENTRYPOINT ["python3.10", "space_runner.py"]
27
+
28
+ CMD ["python3.10", "wave_tank_surgeon.py", \
29
+ "--model", "huihui-ai/Llama-3.2-11B-Vision-Instruct-abliterated", \
30
+ "--ffn-grid", "16", \
31
+ "--attn-grid", "16", \
32
+ "--steps", "750", \
33
+ "--lr", "0.001", \
34
+ "--device", "cuda:0", \
35
+ "--dtype", "float16", \
36
+ "--push-to-hub", "Jibbalit/Llama-3.2-11B-Vision-Instruct-abliterated-WaveTank"]
README.md CHANGED
@@ -1,8 +1,8 @@
1
  ---
2
- title: Wave Tank Surgeon Llama 32 11b Vision Abliterated
3
- emoji: 🚀
4
- colorFrom: indigo
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
8
  ---
 
1
  ---
2
+ title: Wave Tank Surgeon
3
+ emoji: 📊
4
+ colorFrom: green
5
+ colorTo: purple
6
  sdk: docker
7
  pinned: false
8
  ---
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ torch==2.5.0
2
+ transformers>=4.36.0
3
+ accelerate>=0.24.0
4
+ huggingface-hub>=0.19.0
space_runner.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import json
3
+ import os
4
+ import signal
5
+ import subprocess
6
+ import sys
7
+ import threading
8
+ from collections import deque
9
+ from http.server import BaseHTTPRequestHandler, HTTPServer
10
+
11
+
12
+ def main():
13
+ cmd = sys.argv[1:]
14
+ if not cmd:
15
+ print("No command provided to space_runner.py", flush=True)
16
+ sys.exit(2)
17
+
18
+ # Start workload with captured output so we can expose failure tracebacks.
19
+ child = subprocess.Popen(
20
+ cmd,
21
+ stdout=subprocess.PIPE,
22
+ stderr=subprocess.STDOUT,
23
+ text=True,
24
+ bufsize=1,
25
+ )
26
+
27
+ recent_logs = deque(maxlen=400)
28
+
29
+ state = {
30
+ "status": "running",
31
+ "pid": child.pid,
32
+ "returncode": None,
33
+ "command": cmd,
34
+ }
35
+
36
+ def stream_output():
37
+ if child.stdout is None:
38
+ return
39
+ for line in child.stdout:
40
+ line = line.rstrip("\n")
41
+ recent_logs.append(line)
42
+ print(line, flush=True)
43
+
44
+ class Handler(BaseHTTPRequestHandler):
45
+ def do_GET(self):
46
+ payload = json.dumps({**state, "recent_logs": list(recent_logs)}).encode("utf-8")
47
+ self.send_response(200)
48
+ self.send_header("Content-Type", "application/json")
49
+ self.send_header("Content-Length", str(len(payload)))
50
+ self.end_headers()
51
+ self.wfile.write(payload)
52
+
53
+ def log_message(self, fmt, *args):
54
+ return
55
+
56
+ server = HTTPServer(("0.0.0.0", int(os.environ.get("PORT", "7860"))), Handler)
57
+
58
+ log_thread = threading.Thread(target=stream_output, daemon=True)
59
+ log_thread.start()
60
+
61
+ def wait_child():
62
+ rc = child.wait()
63
+ state["returncode"] = rc
64
+ state["status"] = "completed" if rc == 0 else "failed"
65
+
66
+ watcher = threading.Thread(target=wait_child, daemon=True)
67
+ watcher.start()
68
+
69
+ def forward_signal(signum, _frame):
70
+ if child.poll() is None:
71
+ child.send_signal(signum)
72
+
73
+ signal.signal(signal.SIGTERM, forward_signal)
74
+ signal.signal(signal.SIGINT, forward_signal)
75
+
76
+ print("Health server listening on 0.0.0.0:{}".format(os.environ.get("PORT", "7860")), flush=True)
77
+ print("Executing workload: {}".format(" ".join(cmd)), flush=True)
78
+
79
+ server.serve_forever()
80
+
81
+
82
+ if __name__ == "__main__":
83
+ main()
wave_tank_surgeon.py ADDED
@@ -0,0 +1,888 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # /// script
3
+ # dependencies = [
4
+ # "torch",
5
+ # "transformers",
6
+ # "accelerate",
7
+ # "huggingface_hub",
8
+ # ]
9
+ # ///
10
+ """
11
+ wave_tank_surgeon.py - Replace FFN and Attention layers in ANY HuggingFace model
12
+ with wave tank processors. Scans model architecture, trains wave tank replacements
13
+ to mimic each layer's behavior, swaps them in, saves the surgically modified model.
14
+
15
+ Usage:
16
+ python wave_tank_surgeon.py --model deepseek-ai/DeepSeek-R1-Distill-Qwen-14B
17
+ python wave_tank_surgeon.py --model ./my_local_model --ffn-grid 16 --attn-grid 4
18
+ python wave_tank_surgeon.py --model gpt2 --steps 500 --device cuda:0
19
+
20
+ Architecture (verified on DeepSeek-R1-Distill-Qwen-14B, 94.3% size reduction):
21
+ - WaveTankFFN: grid=16, 98.8% param reduction, converges step ~200
22
+ - WaveTankAttention: grid=4 per-head, 79.2% param reduction, converges step ~300
23
+ - StatelessWaveTank core: 2D wave equation via 5-point stencil Laplacian
24
+ """
25
+
26
+ import argparse
27
+ import json
28
+ import os
29
+ import sys
30
+ import time
31
+ import math
32
+ from collections import OrderedDict
33
+ from typing import Dict, List, Optional, Tuple
34
+
35
+ import torch
36
+ import torch.nn as nn
37
+ import torch.nn.functional as F
38
+ from torch.utils.data import DataLoader, TensorDataset
39
+
40
+
41
+ # ============================================================================
42
+ # WAVE TANK CORE
43
+ # ============================================================================
44
+
45
+ class StatelessWaveTank(nn.Module):
46
+ """2D wave equation processor. Nonlinear compute primitive.
47
+
48
+ ∂²u/∂t² = c² ∇²u + S(x,y,t)
49
+
50
+ Uses 5-point stencil Laplacian with learned alpha (wave speed) and damping.
51
+ Runs n_steps of wave dynamics on a grid_size x grid_size excitation field.
52
+ """
53
+ def __init__(self, grid_size=16, wave_alpha=0.1, n_steps=3, damping=0.99):
54
+ super().__init__()
55
+ self.grid_size = grid_size
56
+ self.n_steps = n_steps
57
+ self.alpha = nn.Parameter(torch.tensor(wave_alpha))
58
+ self.damping = nn.Parameter(torch.tensor(damping))
59
+
60
+ def forward(self, excitation):
61
+ # excitation: [..., grid_size, grid_size]
62
+ u = torch.zeros_like(excitation)
63
+ u_prev = torch.zeros_like(excitation)
64
+ for _ in range(self.n_steps):
65
+ laplacian = (
66
+ torch.roll(u_prev, 1, -2) + torch.roll(u_prev, -1, -2) +
67
+ torch.roll(u_prev, 1, -1) + torch.roll(u_prev, -1, -1) -
68
+ 4 * u_prev
69
+ )
70
+ u_next = self.damping * (2 * u - u_prev + self.alpha * laplacian) + excitation
71
+ u_prev = u
72
+ u = u_next
73
+ return u
74
+
75
+
76
+ # ============================================================================
77
+ # WAVE TANK FFN REPLACEMENT
78
+ # ============================================================================
79
+
80
+ class WaveTankFFN(nn.Module):
81
+ """Drop-in FFN replacement using wave tank processor.
82
+
83
+ input → [Linear proj d_model → grid*grid] → [WaveTank] → [Linear map grid*grid → d_model] → output
84
+
85
+ Grid=16 (256 cells) is REQUIRED for FFN — grid=4 fails to approximate
86
+ high-dimensional SwiGLU hidden states.
87
+ """
88
+ def __init__(self, d_model, d_ff=None, grid_size=16, n_steps=3):
89
+ super().__init__()
90
+ self.d_model = d_model
91
+ self.grid_size = grid_size
92
+ self.grid_dim = grid_size * grid_size
93
+ self.in_proj = nn.Linear(d_model, self.grid_dim, bias=False)
94
+ self.wave_tank = StatelessWaveTank(grid_size=grid_size, n_steps=n_steps)
95
+ self.out_proj = nn.Linear(self.grid_dim, d_model, bias=False)
96
+ self.act = nn.SiLU()
97
+
98
+ def forward(self, x):
99
+ B, S, D = x.shape
100
+ N = B * S
101
+ h = self.act(self.in_proj(x)) # [B*S, grid_dim]
102
+ h = h.view(N, self.grid_size, self.grid_size) # [N, G, G]
103
+ h = self.wave_tank(h) # [N, G, G]
104
+ h = h.view(B, S, self.grid_dim) # [B, S, grid_dim]
105
+ return self.out_proj(h) # [B, S, d_model]
106
+
107
+
108
+ # ============================================================================
109
+ # WAVE TANK ATTENTION REPLACEMENT
110
+ # ============================================================================
111
+
112
+ class WaveTankAttention(nn.Module):
113
+ """Drop-in attention replacement using wave tank processor.
114
+
115
+ Per-head Q/K/V projections into grid space are MANDATORY.
116
+ Shared grid across heads fails (loss plateaus at 0.749).
117
+ Grid=4 per-head works (79.2% reduction). Grid=8 also works for more capacity.
118
+
119
+ Flow:
120
+ x → per-head Q/K/V proj → reshape to [N, H, G, G] → wave tank each
121
+ → compute attention scores via einsum → softmax → weighted sum → out_proj
122
+ """
123
+ def __init__(self, d_model, n_heads, grid_size=4, n_steps=3):
124
+ super().__init__()
125
+ self.d_model = d_model
126
+ self.n_heads = n_heads
127
+ self.grid_size = grid_size
128
+ self.grid_dim = grid_size * grid_size
129
+ self.head_dim = self.grid_dim # each head maps to grid
130
+
131
+ self.q_proj = nn.Linear(d_model, n_heads * self.grid_dim, bias=False)
132
+ self.k_proj = nn.Linear(d_model, n_heads * self.grid_dim, bias=False)
133
+ self.v_proj = nn.Linear(d_model, n_heads * self.grid_dim, bias=False)
134
+ self.wave_tank = StatelessWaveTank(grid_size=grid_size, n_steps=n_steps)
135
+ self.out_proj = nn.Linear(n_heads * self.grid_dim, d_model, bias=False)
136
+ self.act = nn.SiLU()
137
+ self.norm = nn.LayerNorm(n_heads * self.grid_dim)
138
+
139
+ # Causal mask will be created dynamically
140
+ self._causal_mask = None
141
+ self._causal_mask_size = 0
142
+ self.return_tuple = False
143
+ self.output_tuple_len = 0
144
+
145
+ def _get_causal_mask(self, S, device, dtype):
146
+ if self._causal_mask is None or self._causal_mask_size < S:
147
+ self._causal_mask_size = S
148
+ mask = torch.triu(torch.full((S, S), float('-inf'), dtype=dtype, device=device), diagonal=1)
149
+ self._causal_mask = mask
150
+ return self._causal_mask[:S, :S]
151
+
152
+ def forward(self, x=None, attention_mask=None, *args, **kwargs):
153
+ if x is None:
154
+ x = kwargs.get('hidden_states')
155
+ if x is None and args:
156
+ x = args[0]
157
+ if x is None:
158
+ raise ValueError("WaveTankAttention requires hidden_states as positional x or keyword hidden_states")
159
+ if attention_mask is None:
160
+ attention_mask = kwargs.get('attention_mask')
161
+
162
+ B, S, D = x.shape
163
+ gs = self.grid_size
164
+ H = self.n_heads
165
+ gd = self.grid_dim
166
+ N = B * S
167
+
168
+ Q = self.act(self.q_proj(x)).view(N, H, gs, gs)
169
+ K = self.act(self.k_proj(x)).view(N, H, gs, gs)
170
+ V = self.act(self.v_proj(x)).view(N, H, gs, gs)
171
+
172
+ # Wave tank processes each head's Q, K, V
173
+ Q_w = self.wave_tank(Q).view(B, S, H, gd)
174
+ K_w = self.wave_tank(K).view(B, S, H, gd)
175
+ V_w = self.wave_tank(V).view(B, S, H, gd)
176
+
177
+ # Attention scores: [B, S, T, H] where T=S (self-attention)
178
+ scores = torch.einsum('bshd,bthd->bsth', Q_w, K_w) / math.sqrt(gd)
179
+
180
+ # Causal mask
181
+ causal = self._get_causal_mask(S, x.device, x.dtype)
182
+ scores = scores + causal.unsqueeze(0).unsqueeze(-1) # [1, S, S, 1]
183
+
184
+ # Softmax over source sequence dimension (dim=2)
185
+ weights = F.softmax(scores, dim=2)
186
+
187
+ # Weighted sum
188
+ out = torch.einsum('bsth,bthd->bshd', weights, V_w) # [B, S, H, gd]
189
+ out = self.norm(self.act(out.reshape(B, S, H * gd)))
190
+ out = self.out_proj(out)
191
+
192
+ if self.return_tuple:
193
+ tuple_len = max(self.output_tuple_len, 1)
194
+ return tuple([out] + [None] * (tuple_len - 1))
195
+ return out
196
+
197
+
198
+ # ============================================================================
199
+ # MODEL SCANNER - Detect FFN and Attention layers in any HF model
200
+ # ============================================================================
201
+
202
+ class LayerInfo:
203
+ def __init__(self, path, layer_type, d_model, d_ff=None, n_heads=None, layer=None):
204
+ self.path = path
205
+ self.layer_type = layer_type # 'ffn' or 'attention'
206
+ self.d_model = d_model
207
+ self.d_ff = d_ff
208
+ self.n_heads = n_heads
209
+ self.layer = layer # reference to the actual nn.Module
210
+ self.output_is_tuple = False
211
+ self.output_tuple_len = 0
212
+
213
+ def __repr__(self):
214
+ extra = f", d_ff={self.d_ff}" if self.d_ff else ""
215
+ extra += f", n_heads={self.n_heads}" if self.n_heads else ""
216
+ return f"LayerInfo({self.path}, {self.layer_type}, d_model={self.d_model}{extra})"
217
+
218
+
219
+ def scan_model(model) -> List[LayerInfo]:
220
+ """Scan any HuggingFace model and identify FFN and attention layers.
221
+
222
+ Works with:
223
+ - GPT-2 family (Conv1D based)
224
+ - LLaMA / Mistral / Qwen family (MLP + Attention)
225
+ - DeepSeek family (MLP + Attention)
226
+ - Gemma family
227
+ - Most other transformer architectures
228
+
229
+ Returns list of LayerInfo objects with enough metadata to create replacements.
230
+ """
231
+ layers = []
232
+ name_to_module = dict(model.named_modules())
233
+
234
+ # Strategy: walk named modules, detect known patterns
235
+ # Track which modules are parents of detected layers to avoid double-counting
236
+ detected_paths = set()
237
+ all_modules = list(model.named_modules())
238
+
239
+ # Sort by path length (most specific first) so we detect leaf layers before parents
240
+ all_modules.sort(key=lambda x: x[0].count('.'), reverse=True)
241
+
242
+ for name, module in all_modules:
243
+ if not name: # skip root
244
+ continue
245
+
246
+ # Skip if this module is a parent of an already-detected layer
247
+ if any(p.startswith(name + '.') for p in detected_paths):
248
+ continue
249
+
250
+ cls_name = type(module).__name__.lower()
251
+
252
+ # --- Detect FFN / MLP layers ---
253
+ if _is_ffn(module, cls_name):
254
+ info = _extract_ffn_info(name, module, model)
255
+ if info:
256
+ layers.append(info)
257
+ detected_paths.add(name)
258
+ continue
259
+
260
+ # --- Detect Attention layers ---
261
+ if _is_attention(module, cls_name):
262
+ info = _extract_attn_info(name, module, model)
263
+ if info:
264
+ layers.append(info)
265
+ detected_paths.add(name)
266
+ continue
267
+
268
+ return layers
269
+
270
+
271
+ def _is_ffn(module, cls_name):
272
+ """Check if module is an FFN/MLP block."""
273
+ ffn_names = {'mlp', 'ffn', 'feedforward', 'feed_forward'}
274
+ if cls_name in ffn_names:
275
+ return True
276
+ # Check for typical FFN structure: has 2+ linear layers and activation
277
+ if hasattr(module, 'gate_proj') or hasattr(module, 'up_proj') or hasattr(module, 'down_proj'):
278
+ return True
279
+ if hasattr(module, 'fc1') and hasattr(module, 'fc2'):
280
+ return True
281
+ if hasattr(module, 'c_fc') and hasattr(module, 'c_proj'):
282
+ return True # GPT-2 style
283
+ return False
284
+
285
+
286
+ def _is_attention(module, cls_name):
287
+ """Check if module is an attention block."""
288
+ attn_names = {'attention', 'selfattention', 'self_attention', 'sdpaattention',
289
+ 'eagerattention', 'gqaattention', 'attentionblock'}
290
+ if cls_name in attn_names:
291
+ return True
292
+ # Reject parent decoder layers that merely contain attention
293
+ if hasattr(module, 'self_attn') and (hasattr(module, 'mlp') or hasattr(module, 'ffn')):
294
+ return False
295
+ if hasattr(module, 'attn') and (hasattr(module, 'mlp') or hasattr(module, 'ffn')):
296
+ return False
297
+ # Has Q/K/V or Q+K+V combined projection
298
+ if (hasattr(module, 'q_proj') or hasattr(module, 'query') or
299
+ hasattr(module, 'c_attn')):
300
+ return True
301
+ # GPT-2 Conv1D attention
302
+ if hasattr(module, 'c_attn') and hasattr(module, 'c_proj'):
303
+ return True
304
+ return False
305
+
306
+
307
+ def _extract_ffn_info(name, module, model):
308
+ """Extract FFN metadata from the module."""
309
+ d_model = None
310
+ d_ff = None
311
+
312
+ # LLaMA/Mistral/DeepSeek style: gate_proj, up_proj, down_proj (SwiGLU)
313
+ if hasattr(module, 'gate_proj'):
314
+ d_model = module.down_proj.out_features if hasattr(module.down_proj, 'out_features') else None
315
+ d_ff = module.gate_proj.out_features if hasattr(module.gate_proj, 'out_features') else None
316
+ if d_model is None:
317
+ # Try to infer from weight shape
318
+ w = module.down_proj.weight
319
+ d_model = w.shape[0] if w.shape[0] < w.shape[1] else w.shape[1]
320
+ return LayerInfo(name, 'ffn', d_model, d_ff=d_ff, layer=module)
321
+
322
+ # GPT-2 style: c_fc, c_proj (Conv1D)
323
+ if hasattr(module, 'c_fc') and hasattr(module, 'c_proj'):
324
+ # Conv1D weight shape: [in_features, out_features]
325
+ # c_fc: [d_model, d_ff], c_proj: [d_ff, d_model]
326
+ d_model = module.c_fc.weight.shape[0]
327
+ d_ff = module.c_fc.weight.shape[1]
328
+ return LayerInfo(name, 'ffn', d_model, d_ff=d_ff, layer=module)
329
+
330
+ # Standard style: fc1, fc2 or similar
331
+ if hasattr(module, 'fc1') and hasattr(module, 'fc2'):
332
+ d_model = module.fc2.out_features if hasattr(module.fc2, 'out_features') else None
333
+ d_ff = module.fc1.out_features if hasattr(module.fc1, 'out_features') else None
334
+ return LayerInfo(name, 'ffn', d_model, d_ff=d_ff, layer=module)
335
+
336
+ # Generic: look for linear layers inside
337
+ linears = [(n, m) for n, m in module.named_modules() if isinstance(m, nn.Linear)]
338
+ if len(linears) >= 2:
339
+ # First linear: d_model → d_ff, last linear: d_ff → d_model
340
+ first = linears[0][1]
341
+ last = linears[-1][1]
342
+ d_ff = first.out_features
343
+ d_model = last.out_features
344
+ return LayerInfo(name, 'ffn', d_model, d_ff=d_ff, layer=module)
345
+
346
+ return None
347
+
348
+
349
+ def _extract_attn_info(name, module, model):
350
+ """Extract attention metadata from the module."""
351
+ d_model = None
352
+ n_heads = None
353
+
354
+ # Try common attribute names for number of heads
355
+ for attr in ['num_heads', 'n_heads', 'num_attention_heads']:
356
+ if hasattr(module, attr):
357
+ n_heads = getattr(module, attr)
358
+ break
359
+
360
+ # Try to get d_model from config or projection dimensions
361
+ for attr in ['embed_dim', 'hidden_size', 'd_model']:
362
+ if hasattr(module, attr):
363
+ d_model = getattr(module, attr)
364
+ break
365
+
366
+ # Infer from weight shapes
367
+ if d_model is None:
368
+ if hasattr(module, 'q_proj') and isinstance(module.q_proj, nn.Linear):
369
+ d_model = module.q_proj.in_features
370
+ elif hasattr(module, 'c_attn'): # GPT-2 Conv1D
371
+ d_model = module.c_attn.weight.shape[1]
372
+ elif hasattr(module, 'query') and isinstance(module.query, nn.Linear):
373
+ d_model = module.query.in_features
374
+
375
+ # Infer n_heads if not found
376
+ if n_heads is None:
377
+ if d_model is not None:
378
+ # Default: assume head_dim=64 or 128
379
+ if d_model % 128 == 0:
380
+ n_heads = d_model // 128
381
+ elif d_model % 64 == 0:
382
+ n_heads = d_model // 64
383
+ else:
384
+ n_heads = max(1, d_model // 96)
385
+
386
+ if d_model is not None and n_heads is not None:
387
+ return LayerInfo(name, 'attention', d_model, n_heads=n_heads, layer=module)
388
+
389
+ return None
390
+
391
+
392
+ # ============================================================================
393
+ # TRAINING: Extract IO pairs from original layers, train wave tank replacements
394
+ # ============================================================================
395
+
396
+ def collect_io_pairs(layer_info, model, dataloader, device, max_batches=50):
397
+ """Run forward passes through the model, hooking into the target layer
398
+ to collect input/output pairs for training the wave tank replacement."""
399
+ inputs_list = []
400
+ outputs_list = []
401
+
402
+ layer = layer_info.layer
403
+ path = layer_info.path
404
+
405
+ hook_registered = False
406
+ captured = {}
407
+
408
+ def first_tensor(value):
409
+ if torch.is_tensor(value):
410
+ return value
411
+ if isinstance(value, dict):
412
+ for key in ('hidden_states', 'last_hidden_state'):
413
+ tensor = value.get(key)
414
+ if torch.is_tensor(tensor):
415
+ return tensor
416
+ for item in value.values():
417
+ tensor = first_tensor(item)
418
+ if tensor is not None:
419
+ return tensor
420
+ if isinstance(value, (tuple, list)):
421
+ for item in value:
422
+ tensor = first_tensor(item)
423
+ if tensor is not None:
424
+ return tensor
425
+ if hasattr(value, 'to_tuple'):
426
+ return first_tensor(value.to_tuple())
427
+ return None
428
+
429
+ def hook_fn(module, input_args, input_kwargs, output):
430
+ input_tensor = first_tensor(input_args)
431
+ if input_tensor is None:
432
+ input_tensor = first_tensor(input_kwargs)
433
+ output_tensor = first_tensor(output)
434
+ if input_tensor is None or output_tensor is None:
435
+ return
436
+ captured['input'] = input_tensor.detach()
437
+ captured['output'] = output_tensor.detach()
438
+ captured['output_is_tuple'] = isinstance(output, tuple)
439
+ captured['output_tuple_len'] = len(output) if isinstance(output, tuple) else 0
440
+
441
+ try:
442
+ handle = layer.register_forward_hook(hook_fn, with_kwargs=True)
443
+ except TypeError:
444
+ def legacy_hook_fn(module, input_args, output):
445
+ hook_fn(module, input_args, {}, output)
446
+ handle = layer.register_forward_hook(legacy_hook_fn)
447
+ hook_registered = True
448
+
449
+ model.eval()
450
+ with torch.no_grad():
451
+ for batch_idx, batch in enumerate(dataloader):
452
+ if batch_idx >= max_batches:
453
+ break
454
+ try:
455
+ # Move batch to device
456
+ if isinstance(batch, dict):
457
+ batch = {k: v.to(device) for k, v in batch.items()}
458
+ model(**batch)
459
+ elif isinstance(batch, (list, tuple)):
460
+ batch = tuple(v.to(device) for v in batch)
461
+ model(*batch)
462
+ else:
463
+ batch = batch.to(device)
464
+ model(batch)
465
+
466
+ if 'input' in captured and 'output' in captured:
467
+ layer_info.output_is_tuple = captured.get('output_is_tuple', False)
468
+ layer_info.output_tuple_len = captured.get('output_tuple_len', 0)
469
+ inputs_list.append(captured['input'].cpu())
470
+ outputs_list.append(captured['output'].cpu())
471
+ captured.clear()
472
+ except Exception as e:
473
+ print(f" Warning: batch {batch_idx} failed: {e}")
474
+ continue
475
+
476
+ if hook_registered:
477
+ handle.remove()
478
+
479
+ if not inputs_list:
480
+ print(f" ERROR: No IO pairs collected for {path}")
481
+ return None, None
482
+
483
+ all_inputs = torch.cat(inputs_list, dim=0)
484
+ all_outputs = torch.cat(outputs_list, dim=0)
485
+ print(f" Collected {all_inputs.shape[0]} samples, input shape {all_inputs.shape}, output shape {all_outputs.shape}")
486
+ return all_inputs, all_outputs
487
+
488
+
489
+ def train_replacement(layer_info, inputs, outputs, device, ffn_grid=16, attn_grid=4,
490
+ steps=5000, lr=1e-3, batch_size=32):
491
+ """Train a wave tank replacement to mimic the original layer's behavior."""
492
+ path = layer_info.path
493
+ d_model = layer_info.d_model
494
+ layer_type = layer_info.layer_type
495
+ target_dtype = inputs.dtype if inputs.is_floating_point() else torch.float32
496
+ train_dtype = torch.float32
497
+
498
+ # Create replacement module
499
+ if layer_type == 'ffn':
500
+ replacement = WaveTankFFN(d_model=d_model, grid_size=ffn_grid, n_steps=3).to(
501
+ device=device,
502
+ dtype=train_dtype,
503
+ )
504
+ label = f"WaveTankFFN(grid={ffn_grid})"
505
+ elif layer_type == 'attention':
506
+ n_heads = layer_info.n_heads
507
+ replacement = WaveTankAttention(d_model=d_model, n_heads=n_heads,
508
+ grid_size=attn_grid, n_steps=3).to(
509
+ device=device,
510
+ dtype=train_dtype,
511
+ )
512
+ label = f"WaveTankAttn(grid={attn_grid}, heads={n_heads})"
513
+ else:
514
+ return None
515
+
516
+ # Count params
517
+ orig_params = sum(p.numel() for p in layer_info.layer.parameters())
518
+ repl_params = sum(p.numel() for p in replacement.parameters())
519
+ reduction = 1.0 - repl_params / max(orig_params, 1)
520
+ print(f" {label}: {repl_params:,} params vs {orig_params:,} original ({reduction*100:.1f}% reduction)")
521
+
522
+ # Prepare data
523
+ dataset = TensorDataset(inputs, outputs)
524
+ loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, drop_last=True)
525
+
526
+ optimizer = torch.optim.Adam(replacement.parameters(), lr=lr)
527
+ loss_fn = nn.MSELoss()
528
+
529
+ # Flatten outputs for FFN (attention outputs may have different shapes)
530
+ # Ensure inputs/outputs are [batch, seq, d_model]
531
+ if inputs.dim() == 2:
532
+ inputs = inputs.unsqueeze(1)
533
+ outputs = outputs.unsqueeze(1)
534
+
535
+ replacement.train()
536
+ best_loss = float('inf')
537
+ best_state = None
538
+
539
+ for step in range(steps):
540
+ epoch_loss = 0.0
541
+ n_batches = 0
542
+ for batch_in, batch_out in loader:
543
+ batch_in = batch_in.to(device=device, dtype=train_dtype)
544
+ batch_out = batch_out.to(device=device, dtype=train_dtype)
545
+
546
+ optimizer.zero_grad()
547
+
548
+ if layer_type == 'ffn':
549
+ pred = replacement(batch_in)
550
+ else:
551
+ # Attention: may need to handle residual vs direct output
552
+ try:
553
+ pred = replacement(batch_in)
554
+ except Exception:
555
+ continue
556
+
557
+ loss = loss_fn(pred, batch_out)
558
+ loss.backward()
559
+
560
+ # Gradient clipping for stability
561
+ torch.nn.utils.clip_grad_norm_(replacement.parameters(), 1.0)
562
+ optimizer.step()
563
+
564
+ epoch_loss += loss.item()
565
+ n_batches += 1
566
+
567
+ avg_loss = epoch_loss / max(n_batches, 1)
568
+
569
+ if avg_loss < best_loss:
570
+ best_loss = avg_loss
571
+ best_state = {k: v.clone() for k, v in replacement.state_dict().items()}
572
+
573
+ if (step + 1) % 50 == 0 or step == 0:
574
+ alpha = replacement.wave_tank.alpha.item()
575
+ damping = replacement.wave_tank.damping.item()
576
+ print(f" Step {step+1}/{steps}: loss={avg_loss:.6f} (best={best_loss:.6f}) "
577
+ f"alpha={alpha:.4f} damping={damping:.4f}")
578
+
579
+ # Load best weights
580
+ if best_state is not None:
581
+ replacement.load_state_dict(best_state)
582
+
583
+ if layer_type == 'attention':
584
+ replacement.return_tuple = layer_info.output_is_tuple
585
+ replacement.output_tuple_len = layer_info.output_tuple_len
586
+
587
+ return replacement.to(device=device, dtype=target_dtype)
588
+
589
+
590
+ # ============================================================================
591
+ # SURGERY: Replace layers in the model
592
+ # ============================================================================
593
+
594
+ def perform_surgery(model, layer_info, replacement):
595
+ """Replace a layer in the model with its wave tank replacement.
596
+
597
+ Navigates the module hierarchy using dot-separated path and swaps the module.
598
+ """
599
+ path = layer_info.path
600
+ parts = path.split('.')
601
+
602
+ # Navigate to parent module
603
+ parent = model
604
+ for part in parts[:-1]:
605
+ if hasattr(parent, part):
606
+ parent = getattr(parent, part)
607
+ elif isinstance(parent, nn.ModuleList) or isinstance(parent, nn.ModuleDict):
608
+ parent = parent[int(part)] if part.isdigit() else parent[part]
609
+ else:
610
+ raise ValueError(f"Cannot navigate to {path}: stuck at {'.'.join(parts[:parts.index(part)])}")
611
+
612
+ final_name = parts[-1]
613
+
614
+ # Set the replacement
615
+ if isinstance(parent, nn.ModuleList):
616
+ setattr(parent, final_name, replacement) # ModuleList items set via __setattr__
617
+ elif isinstance(parent, nn.ModuleDict):
618
+ parent[final_name] = replacement
619
+ else:
620
+ setattr(parent, final_name, replacement)
621
+
622
+ print(f" Surgery: replaced {path} with {type(replacement).__name__}")
623
+
624
+
625
+ # ============================================================================
626
+ # MAIN: Full pipeline
627
+ # ============================================================================
628
+
629
+ def main():
630
+ parser = argparse.ArgumentParser(description="Wave Tank Surgeon - Replace FFN/Attention in any model")
631
+ parser.add_argument('--model', required=True, help='HuggingFace model name or local path')
632
+ parser.add_argument('--output', default=None, help='Output directory for replaced model')
633
+ parser.add_argument('--ffn-grid', type=int, default=16, help='Grid size for FFN replacement (default: 16)')
634
+ parser.add_argument('--attn-grid', type=int, default=16, help='Grid size for attention replacement (default: 4)')
635
+ parser.add_argument('--steps', type=int, default=5000, help='Training steps per layer (default: 500)')
636
+ parser.add_argument('--lr', type=float, default=1e-3, help='Learning rate (default: 1e-3)')
637
+ parser.add_argument('--batch-size', type=int, default=16, help='Batch size for IO collection (default: 4)')
638
+ parser.add_argument('--max-batches', type=int, default=500, help='Max batches for IO pair collection (default: 50)')
639
+ parser.add_argument('--device', default=None, help='Device (default: auto-detect)')
640
+ parser.add_argument('--skip-attn', action='store_true', help='Skip attention replacement (FFN only)')
641
+ parser.add_argument('--skip-ffn', action='store_true', help='Skip FFN replacement (attention only)')
642
+ parser.add_argument('--dtype', default='float16', help='Model dtype: float16, float32, bfloat16')
643
+ parser.add_argument('--dry-run', action='store_true', help='Scan model and report layers, but do not train')
644
+ parser.add_argument('--push-to-hub', default=None,
645
+ help='Push replaced model to HF Hub (e.g. username/model-wave-tank)')
646
+ parser.add_argument('--private', action='store_true', help='Make pushed Hub repo private')
647
+ args = parser.parse_args()
648
+
649
+ # Device
650
+ if args.device:
651
+ device = torch.device(args.device)
652
+ elif torch.cuda.is_available():
653
+ device = torch.device('cuda:0')
654
+ else:
655
+ device = torch.device('cpu')
656
+
657
+ # Dtype
658
+ dtype_map = {
659
+ 'float16': torch.float16, 'fp16': torch.float16,
660
+ 'float32': torch.float32, 'fp32': torch.float32,
661
+ 'bfloat16': torch.bfloat16, 'bf16': torch.bfloat16,
662
+ }
663
+ torch_dtype = dtype_map.get(args.dtype, torch.float16)
664
+
665
+ print("=" * 70)
666
+ print("WAVE TANK SURGEON")
667
+ print(f"Model: {args.model}")
668
+ print(f"Device: {device} | Dtype: {torch_dtype}")
669
+ print(f"FFN grid: {args.ffn_grid} | Attn grid: {args.attn_grid}")
670
+ print(f"Training: {args.steps} steps, lr={args.lr}")
671
+ print("=" * 70)
672
+
673
+ hf_token = (
674
+ os.environ.get('HF_TOKEN')
675
+ or os.environ.get('HUGGING_FACE_HUB_TOKEN')
676
+ or os.environ.get('HUGGINGFACEHUB_API_TOKEN')
677
+ )
678
+
679
+ # Load model and tokenizer
680
+ print(f"Loading model: {args.model}...")
681
+ from transformers import AutoModelForCausalLM, AutoTokenizer
682
+
683
+ tokenizer = AutoTokenizer.from_pretrained(
684
+ args.model,
685
+ trust_remote_code=True,
686
+ token=hf_token,
687
+ )
688
+ model = AutoModelForCausalLM.from_pretrained(
689
+ args.model,
690
+ torch_dtype=torch_dtype,
691
+ device_map='auto' if torch.cuda.is_available() else None,
692
+ trust_remote_code=True,
693
+ token=hf_token,
694
+ )
695
+
696
+ if device.type == 'cpu' or (device.type == 'cuda' and not hasattr(model, 'hf_device_map')):
697
+ model = model.to(device)
698
+
699
+ print(f"Model loaded. Total params: {sum(p.numel() for p in model.parameters()):,}")
700
+
701
+ # Scan model
702
+ print(f"Scanning model architecture...")
703
+ layer_infos = scan_model(model)
704
+
705
+ ffn_layers = [l for l in layer_infos if l.layer_type == 'ffn']
706
+ attn_layers = [l for l in layer_infos if l.layer_type == 'attention']
707
+
708
+ print(f"Found {len(ffn_layers)} FFN layers and {len(attn_layers)} attention layers")
709
+ for l in layer_infos:
710
+ print(f" {l}")
711
+
712
+ if args.dry_run:
713
+ print("Dry run: exiting after scan.")
714
+ return model
715
+
716
+ # Prepare calibration data (use tokenizer to create random inputs)
717
+ print(f"Preparing calibration data...")
718
+ calib_texts = [
719
+ "The wave tank computes interference patterns through the discrete Laplacian.",
720
+ "Attention mechanisms allow models to focus on relevant parts of the input.",
721
+ "The transformer architecture has revolutionized natural language processing.",
722
+ "Wave equations describe the propagation of disturbances through a medium.",
723
+ "Deep learning models can be compressed using knowledge distillation.",
724
+ "The Laplacian operator measures the divergence of the gradient field.",
725
+ "Neural network pruning removes redundant parameters while preserving accuracy.",
726
+ "Quantization reduces the precision of weights to decrease model size.",
727
+ ]
728
+ encoded = tokenizer(
729
+ calib_texts,
730
+ return_tensors='pt',
731
+ padding=True,
732
+ truncation=True,
733
+ max_length=128,
734
+ )
735
+
736
+ # Create dataloader from calibration data
737
+ input_ids = encoded['input_ids']
738
+ attention_mask = encoded['attention_mask']
739
+ # Repeat to get enough batches
740
+ reps = max(args.max_batches, 1)
741
+ input_ids = input_ids.repeat(reps, 1)
742
+ attention_mask = attention_mask.repeat(reps, 1)
743
+
744
+ batch_size = min(args.batch_size, input_ids.shape[0])
745
+ calib_loader = DataLoader(
746
+ TensorDataset(input_ids, attention_mask),
747
+ batch_size=batch_size,
748
+ shuffle=False,
749
+ )
750
+
751
+ # Convert to dict-style loader for HuggingFace models
752
+ class DictLoader:
753
+ def __init__(self, input_ids, attention_mask, batch_size):
754
+ self.input_ids = input_ids
755
+ self.attention_mask = attention_mask
756
+ self.batch_size = batch_size
757
+ self.n = input_ids.shape[0]
758
+
759
+ def __iter__(self):
760
+ for i in range(0, self.n, self.batch_size):
761
+ yield {
762
+ 'input_ids': self.input_ids[i:i+self.batch_size],
763
+ 'attention_mask': self.attention_mask[i:i+self.batch_size],
764
+ }
765
+
766
+ def __len__(self):
767
+ return (self.n + self.batch_size - 1) // self.batch_size
768
+
769
+ dict_loader = DictLoader(input_ids, attention_mask, batch_size)
770
+
771
+ # Train and replace each layer
772
+ replacements = {}
773
+ layers_to_process = []
774
+
775
+ if not args.skip_ffn:
776
+ layers_to_process.extend(ffn_layers)
777
+ if not args.skip_attn:
778
+ layers_to_process.extend(attn_layers)
779
+
780
+ # Sort by path for deterministic order
781
+ layers_to_process.sort(key=lambda l: l.path)
782
+
783
+ for i, layer_info in enumerate(layers_to_process):
784
+ print(f"")
785
+ print(f"[{i+1}/{len(layers_to_process)}] Processing {layer_info.path} ({layer_info.layer_type})")
786
+ print(f" d_model={layer_info.d_model}" +
787
+ (f", d_ff={layer_info.d_ff}" if layer_info.d_ff else "") +
788
+ (f", n_heads={layer_info.n_heads}" if layer_info.n_heads else ""))
789
+
790
+ # Collect IO pairs
791
+ print(f" Collecting IO pairs...")
792
+ inputs, outputs = collect_io_pairs(
793
+ layer_info, model, dict_loader, device,
794
+ max_batches=args.max_batches
795
+ )
796
+
797
+ if inputs is None:
798
+ print(f" SKIPPING: could not collect IO pairs")
799
+ continue
800
+
801
+ # Reshape if needed - ensure [batch, seq, d_model]
802
+ if inputs.dim() == 2:
803
+ inputs = inputs.unsqueeze(1)
804
+ if outputs.dim() == 2:
805
+ outputs = outputs.unsqueeze(1)
806
+
807
+ # For attention layers, outputs might be tuple - just use first element
808
+ # (the actual attention output, not past_key_values etc.)
809
+
810
+ # Train replacement
811
+ grid = args.ffn_grid if layer_info.layer_type == 'ffn' else args.attn_grid
812
+ replacement = train_replacement(
813
+ layer_info, inputs, outputs, device,
814
+ ffn_grid=args.ffn_grid, attn_grid=args.attn_grid,
815
+ steps=args.steps, lr=args.lr,
816
+ )
817
+
818
+ if replacement is not None:
819
+ replacements[layer_info.path] = replacement
820
+ # Replace in model
821
+ replacement.eval()
822
+ perform_surgery(model, layer_info, replacement)
823
+
824
+ # Report
825
+ print(f"")
826
+ print("=" * 70)
827
+ print("SURGERY COMPLETE")
828
+ orig_size = 0
829
+ new_size = 0
830
+ for l in layer_infos:
831
+ op = sum(p.numel() for p in l.layer.parameters())
832
+ orig_size += op
833
+ for p in model.parameters():
834
+ new_size += p.numel()
835
+ print(f"Original total: {orig_size:,} params in replaced layers")
836
+ print(f"Model now: {sum(p.numel() for p in model.parameters()):,} total params")
837
+ print(f"Replaced {len(replacements)}/{len(layers_to_process)} layers")
838
+ print("=" * 70)
839
+
840
+ # Save
841
+ if args.output:
842
+ output_dir = args.output
843
+ else:
844
+ model_name = args.model.replace('/', '_')
845
+ output_dir = f"{model_name}_wave_tank"
846
+
847
+ os.makedirs(output_dir, exist_ok=True)
848
+ print(f"Saving replaced model to {output_dir}...")
849
+ model.save_pretrained(output_dir)
850
+ tokenizer.save_pretrained(output_dir)
851
+
852
+ # Save surgery report
853
+ report = {
854
+ 'source_model': args.model,
855
+ 'ffn_grid': args.ffn_grid,
856
+ 'attn_grid': args.attn_grid,
857
+ 'training_steps': args.steps,
858
+ 'replaced_layers': {p: type(r).__name__ for p, r in replacements.items()},
859
+ 'skipped_layers': [l.path for l in layers_to_process if l.path not in replacements],
860
+ 'ffn_count': len([p for p in replacements.values() if isinstance(p, WaveTankFFN)]),
861
+ 'attn_count': len([p for p in replacements.values() if isinstance(p, WaveTankAttention)]),
862
+ }
863
+ with open(os.path.join(output_dir, 'surgery_report.json'), 'w') as f:
864
+ json.dump(report, f, indent=2)
865
+
866
+ print(f"Saved. Surgery report: {os.path.join(output_dir, 'surgery_report.json')}")
867
+
868
+ # Push to Hub if requested
869
+ if args.push_to_hub:
870
+ print(f"Pushing to HuggingFace Hub: {args.push_to_hub}...")
871
+ model.push_to_hub(args.push_to_hub, private=args.private, token=hf_token)
872
+ tokenizer.push_to_hub(args.push_to_hub, private=args.private, token=hf_token)
873
+ # Also push surgery report
874
+ from huggingface_hub import HfApi
875
+ api = HfApi(token=hf_token)
876
+ api.upload_file(
877
+ path_or_fileobj=os.path.join(output_dir, 'surgery_report.json'),
878
+ path_in_repo='surgery_report.json',
879
+ repo_id=args.push_to_hub,
880
+ repo_type='model',
881
+ )
882
+ print(f"Pushed to Hub: https://huggingface.co/{args.push_to_hub}")
883
+
884
+ return model
885
+
886
+
887
+ if __name__ == '__main__':
888
+ main()