rogermt commited on
Commit
88b0444
·
verified ·
1 Parent(s): b3899cf

Add FP16 graph surgery script (from 6105 public notebook) — batch optimization for all models"

Browse files
Files changed (1) hide show
  1. medal-solvers/fp16_surgery_batch.py +220 -0
medal-solvers/fp16_surgery_batch.py ADDED
@@ -0,0 +1,220 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FP16 Graph Surgery v2 — from the highest public notebook (~6105 LB)
2
+ # Applies to ALL models in a submission zip. Halves intermediate memory by converting float32→float16.
3
+ # ARC grids only hold small integers (0-9, coords ≤30), all exact in float16.
4
+ # This is provably output-preserving and adds ~20-40 pts across all 400 tasks.
5
+ #
6
+ # Usage on Kaggle:
7
+ # python fp16_surgery_batch.py --input submission-base.zip --output submission-fp16.zip
8
+ #
9
+ # Then profile with profile_best_models.py to verify gains.
10
+
11
+ import os, sys, glob, json, zipfile, shutil, tempfile
12
+ import numpy as np
13
+ import onnx
14
+ import onnxruntime as ort
15
+ from onnx import TensorProto, helper, numpy_helper
16
+
17
+ F32, F16 = TensorProto.FLOAT, TensorProto.FLOAT16
18
+ FP16_MAX = 65504.0
19
+ C, H, W = 10, 30, 30
20
+
21
+ def _fits(arr):
22
+ return arr.size == 0 or float(np.nanmax(np.abs(arr))) <= FP16_MAX
23
+
24
+ def _guard(g):
25
+ for init in g.initializer:
26
+ if init.data_type == F32 and not _fits(numpy_helper.to_array(init)):
27
+ raise ValueError('float constant exceeds fp16 range')
28
+ for n in g.node:
29
+ for a in n.attribute:
30
+ if a.type == onnx.AttributeProto.TENSOR and a.t.data_type == F32 and not _fits(numpy_helper.to_array(a.t)):
31
+ raise ValueError('const attr exceeds fp16 range')
32
+
33
+ VALUE_PRESERVING = {'Slice','Gather','Transpose','Reshape','Squeeze','Unsqueeze','Identity'}
34
+
35
+ def fp16_surgery_v2(in_path, out_path):
36
+ """v2: pushes Cast boundary past value-preserving shape ops for maximum saving."""
37
+ m = onnx.load(in_path); g = m.graph; _guard(g)
38
+ init_names = {i.name for i in g.initializer}
39
+
40
+ # Find value-preserving region reachable from input
41
+ region = {'input'}; changed = True
42
+ while changed:
43
+ changed = False
44
+ for n in g.node:
45
+ if n.op_type not in VALUE_PRESERVING: continue
46
+ if any(o in region for o in n.output): continue
47
+ data_ins = [x for x in n.input if x and x not in init_names]
48
+ if data_ins and all(x in region for x in data_ins):
49
+ for o in n.output:
50
+ if o: region.add(o)
51
+ changed = True
52
+ region.discard('output')
53
+
54
+ def is_region_vp(n):
55
+ return n.op_type in VALUE_PRESERVING and any(o in region for o in n.output)
56
+
57
+ # Find boundary tensors
58
+ boundary = set()
59
+ for n in g.node:
60
+ if is_region_vp(n): continue
61
+ for x in n.input:
62
+ if x in region and x != 'input': boundary.add(x)
63
+ if any(x == 'input' for n in g.node if not is_region_vp(n) for x in n.input):
64
+ boundary.add('input')
65
+
66
+ # Convert float32 initializers to float16
67
+ for init in g.initializer:
68
+ if init.data_type == F32:
69
+ init.CopyFrom(numpy_helper.from_array(
70
+ numpy_helper.to_array(init).astype(np.float16), init.name))
71
+
72
+ # Insert Cast nodes at boundary
73
+ cast_map = {t: f'{t}__h16' for t in boundary}
74
+ new_nodes = []
75
+ if 'input' in boundary:
76
+ new_nodes.append(helper.make_node('Cast', ['input'], ['input__h16'], to=F16, name='input__h16'))
77
+ for n in g.node:
78
+ new_nodes.append(n)
79
+ for o in n.output:
80
+ if o in cast_map:
81
+ new_nodes.append(helper.make_node('Cast', [o], [cast_map[o]], to=F16, name=cast_map[o]))
82
+ del g.node[:]; g.node.extend(new_nodes)
83
+
84
+ # Rewire non-VP nodes to use cast outputs
85
+ for n in g.node:
86
+ if n.op_type == 'Cast' and n.output and n.output[0].endswith('__h16'): continue
87
+ if is_region_vp(n): continue
88
+ for i, x in enumerate(n.input):
89
+ if x in cast_map: n.input[i] = cast_map[x]
90
+
91
+ # Fix remaining Cast nodes and const attrs
92
+ for n in g.node:
93
+ if n.op_type == 'Cast' and not (n.output and n.output[0].endswith('__h16')):
94
+ for a in n.attribute:
95
+ if a.name == 'to' and a.i == F32: a.i = F16
96
+ for a in n.attribute:
97
+ if a.type == onnx.AttributeProto.TENSOR and a.t.data_type == F32:
98
+ a.t.CopyFrom(numpy_helper.from_array(
99
+ numpy_helper.to_array(a.t).astype(np.float16), a.t.name))
100
+
101
+ del g.value_info[:]
102
+ for o in g.output:
103
+ if o.type.tensor_type.elem_type == F32: o.type.tensor_type.elem_type = F16
104
+ onnx.checker.check_model(m); onnx.save(m, out_path)
105
+
106
+
107
+ def fp16_surgery_v1(in_path, out_path):
108
+ """v1 fallback: single Cast at input."""
109
+ m = onnx.load(in_path); g = m.graph; _guard(g)
110
+ for init in g.initializer:
111
+ if init.data_type == F32:
112
+ init.CopyFrom(numpy_helper.from_array(
113
+ numpy_helper.to_array(init).astype(np.float16), init.name))
114
+ g.node.insert(0, helper.make_node('Cast', ['input'], ['input_h16'], to=F16, name='input_h16'))
115
+ for n in list(g.node)[1:]:
116
+ for i, x in enumerate(n.input):
117
+ if x == 'input': n.input[i] = 'input_h16'
118
+ for n in g.node:
119
+ if n.op_type == 'Cast' and n.name != 'input_h16':
120
+ for a in n.attribute:
121
+ if a.name == 'to' and a.i == F32: a.i = F16
122
+ for a in n.attribute:
123
+ if a.type == onnx.AttributeProto.TENSOR and a.t.data_type == F32:
124
+ a.t.CopyFrom(numpy_helper.from_array(
125
+ numpy_helper.to_array(a.t).astype(np.float16), a.t.name))
126
+ del g.value_info[:]
127
+ for o in g.output:
128
+ if o.type.tensor_type.elem_type == F32: o.type.tensor_type.elem_type = F16
129
+ onnx.checker.check_model(m); onnx.save(m, out_path)
130
+
131
+
132
+ def encode(grid):
133
+ t = np.zeros((1, C, H, W), np.float32)
134
+ for r, row in enumerate(grid):
135
+ for c, v in enumerate(row):
136
+ if 0 <= int(v) < C: t[0, int(v), r, c] = 1.0
137
+ return t
138
+
139
+ def same_on_examples(p0, p1, exs):
140
+ """Verify both models produce identical outputs on given examples."""
141
+ s0 = ort.InferenceSession(p0, providers=['CPUExecutionProvider'])
142
+ s1 = ort.InferenceSession(p1, providers=['CPUExecutionProvider'])
143
+ for e in exs:
144
+ x = encode(e['input'])
145
+ a = np.asarray(s0.run(None, {s0.get_inputs()[0].name: x})[0])
146
+ b = np.asarray(s1.run(None, {s1.get_inputs()[0].name: x})[0])
147
+ if a.shape != b.shape or not np.array_equal(a.astype(np.float32), b.astype(np.float32)):
148
+ return False
149
+ return True
150
+
151
+
152
+ MIN_SAVE = 1800 # only keep fp16 if it saves at least this many bytes of cost
153
+
154
+ def process_submission(input_zip, output_zip, task_data_dir):
155
+ """Apply FP16 surgery to all models in a submission zip."""
156
+ work_dir = tempfile.mkdtemp()
157
+ src_dir = os.path.join(work_dir, 'src')
158
+ out_dir = os.path.join(work_dir, 'out')
159
+ os.makedirs(src_dir); os.makedirs(out_dir)
160
+
161
+ # Extract input zip
162
+ with zipfile.ZipFile(input_zip, 'r') as z:
163
+ z.extractall(src_dir)
164
+
165
+ kept_fp16 = 0
166
+ total = 0
167
+
168
+ for onnx_path in sorted(glob.glob(os.path.join(src_dir, 'task*.onnx'))):
169
+ name = os.path.basename(onnx_path)
170
+ tid = int(name[4:7])
171
+ dst = os.path.join(out_dir, name)
172
+ total += 1
173
+
174
+ # Load task examples for verification
175
+ task_json = os.path.join(task_data_dir, f'task{tid:03d}.json')
176
+ exs = []
177
+ if os.path.exists(task_json):
178
+ with open(task_json) as f:
179
+ data = json.load(f)
180
+ exs = data.get('train', []) + data.get('test', [])
181
+
182
+ # Try v2 then v1
183
+ best = None
184
+ for fn in (fp16_surgery_v2, fp16_surgery_v1):
185
+ tmp = os.path.join(work_dir, f'_tmp_{fn.__name__}_{name}')
186
+ try:
187
+ fn(onnx_path, tmp)
188
+ if exs and same_on_examples(onnx_path, tmp, exs):
189
+ best = tmp
190
+ break
191
+ except Exception:
192
+ pass
193
+
194
+ if best:
195
+ shutil.copy(best, dst)
196
+ kept_fp16 += 1
197
+ else:
198
+ shutil.copy(onnx_path, dst)
199
+
200
+ # Create output zip
201
+ with zipfile.ZipFile(output_zip, 'w', zipfile.ZIP_DEFLATED) as zf:
202
+ for p in sorted(glob.glob(os.path.join(out_dir, 'task*.onnx'))):
203
+ zf.write(p, arcname=os.path.basename(p))
204
+
205
+ print(f"FP16 surgery applied to {kept_fp16}/{total} tasks")
206
+ print(f"Output: {output_zip} ({os.path.getsize(output_zip)} bytes)")
207
+
208
+ shutil.rmtree(work_dir)
209
+ return kept_fp16
210
+
211
+
212
+ if __name__ == '__main__':
213
+ import argparse
214
+ parser = argparse.ArgumentParser()
215
+ parser.add_argument('--input', default='submission-base.zip')
216
+ parser.add_argument('--output', default='submission-fp16.zip')
217
+ parser.add_argument('--task-data-dir', default='/kaggle/input/competitions/neurogolf-2026')
218
+ args = parser.parse_args()
219
+
220
+ process_submission(args.input, args.output, args.task_data_dir)