| """fix_identity.py — Remove wasteful Identity nodes from ONNX models. |
| |
| The Kaggle scorer counts ALL intermediate tensors except 'input' and 'output'. |
| If a model uses Identity as the last node (Pad → t_N → Identity → output), |
| the t_N tensor ([1,10,30,30] = 36KB) is counted as memory waste. |
| |
| Fix: Rename the penultimate node's output to 'output' and remove Identity. |
| |
| Usage: |
| python fix_identity.py --model optimized/task028.onnx |
| python fix_identity.py --dir optimized/ # Fix all .onnx in directory |
| """ |
| import onnx |
| import os |
| import argparse |
| import math |
| import numpy as np |
|
|
|
|
| def fix_identity(model_path, output_path=None): |
| """Remove trailing Identity node from ONNX model. |
| |
| Returns True if fix was applied, False if no fix needed. |
| """ |
| if output_path is None: |
| output_path = model_path |
| |
| model = onnx.load(model_path) |
| |
| |
| if len(model.graph.node) == 0: |
| return False |
| |
| last_node = model.graph.node[-1] |
| if last_node.op_type != 'Identity': |
| return False |
| if 'output' not in list(last_node.output): |
| return False |
| |
| |
| intermediate_name = last_node.input[0] |
| |
| |
| found = False |
| for node in model.graph.node[:-1]: |
| if intermediate_name in list(node.output): |
| idx = list(node.output).index(intermediate_name) |
| node.output[idx] = 'output' |
| found = True |
| break |
| |
| if not found: |
| return False |
| |
| |
| del model.graph.node[-1] |
| |
| |
| del model.graph.value_info[:] |
| model = onnx.shape_inference.infer_shapes(model, strict_mode=True) |
| |
| onnx.save(model, output_path) |
| return True |
|
|
|
|
| def estimate_savings(model_path): |
| """Estimate memory savings from removing Identity.""" |
| model = onnx.load(model_path) |
| |
| if len(model.graph.node) == 0: |
| return 0 |
| |
| last_node = model.graph.node[-1] |
| if last_node.op_type != 'Identity' or 'output' not in list(last_node.output): |
| return 0 |
| |
| intermediate_name = last_node.input[0] |
| |
| for vi in model.graph.value_info: |
| if vi.name == intermediate_name: |
| if vi.type.HasField('tensor_type') and vi.type.tensor_type.HasField('shape'): |
| dims = [d.dim_value for d in vi.type.tensor_type.shape.dim] |
| dt = onnx.helper.tensor_dtype_to_np_dtype(vi.type.tensor_type.elem_type) |
| return int(np.prod(dims)) * np.dtype(dt).itemsize |
| |
| |
| return 36000 |
|
|
|
|
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser(description='Remove Identity node waste from ONNX models') |
| parser.add_argument('--model', help='Single model to fix') |
| parser.add_argument('--dir', help='Directory of models to fix') |
| parser.add_argument('--dry-run', action='store_true', help='Show savings without modifying') |
| args = parser.parse_args() |
| |
| models = [] |
| if args.model: |
| models.append(args.model) |
| elif args.dir: |
| for f in sorted(os.listdir(args.dir)): |
| if f.endswith('.onnx'): |
| models.append(os.path.join(args.dir, f)) |
| else: |
| parser.print_help() |
| exit(1) |
| |
| total_savings = 0 |
| for model_path in models: |
| savings = estimate_savings(model_path) |
| if savings > 0: |
| if args.dry_run: |
| print(f" {model_path}: would save {savings:,} bytes") |
| else: |
| if fix_identity(model_path): |
| print(f" {model_path}: FIXED (saved {savings:,} bytes)") |
| else: |
| print(f" {model_path}: fix failed") |
| total_savings += savings |
| else: |
| print(f" {model_path}: no Identity to fix") |
| |
| if total_savings > 0: |
| score_improvement = math.log(1 + total_savings / 50000) |
| print(f"\n Total memory savings: {total_savings:,} bytes") |
|
|