File size: 3,540 Bytes
0939cc2
 
 
3644a98
 
 
 
 
 
 
8c12a26
 
0939cc2
 
3644a98
0939cc2
 
 
 
 
 
8c12a26
0939cc2
8c12a26
 
3644a98
0939cc2
8c12a26
 
 
 
 
3644a98
 
 
 
 
8c12a26
3644a98
8c12a26
3644a98
 
 
 
0939cc2
 
 
3644a98
 
 
 
 
8c12a26
3644a98
0939cc2
 
3644a98
8c12a26
3644a98
0939cc2
3644a98
 
 
0939cc2
3644a98
 
 
 
 
 
 
 
0939cc2
 
 
3644a98
0939cc2
 
 
3644a98
 
0939cc2
3644a98
 
 
 
 
 
 
 
0939cc2
 
8c12a26
3644a98
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3
"""Static profiling for ONNX models.

The new neurogolf_utils.py (May 14 2026) changed score_network() to require
(sanitized_model, trace_path) instead of just a filename. This requires running
ORT with profiling enabled, which is too heavy for local model generation.

Strategy: Use static fallback for local scoring during model generation.
Real scoring happens on Kaggle at submission time via the official utils.
Models are NOT rejected locally — they're validated via inference correctness.

Returns (macs=0, memory, params) for backward compatibility with solver_registry.py.
"""

import math
import onnx
from onnx import numpy_helper
from .constants import BANNED_OPS, GH, GW


def score_network(path):
    """Score network locally. Returns (macs, memory, params) or (None, None, None).
    
    macs is always 0 (no longer used in Kaggle scoring since May 4 2026).
    memory and params are static estimates sufficient for local development.
    Real scoring uses ORT profiler on Kaggle.
    """
    result = _static_profile(path)
    if result is None:
        return None, None, None
    memory, params = result
    return 0, memory, params


def estimate_score(path):
    """Estimate score under new formula: 25 - ln(memory + params)."""
    result = _static_profile(path)
    if result is None:
        return None
    memory, params = result
    cost = memory + params
    if cost <= 0:
        return 25.0
    return max(1.0, 25.0 - math.log(cost))


def _static_profile(path):
    """Static profiling: estimate memory + params.
    
    memory = sum of all initializer bytes + estimated intermediate tensor bytes
    params = sum of all initializer element counts + Constant node values
    
    Returns (memory, params) or None if model is invalid.
    """
    try:
        model = onnx.load(path)
    except Exception:
        return None
    
    params = 0
    memory = 0  # bytes
    
    # Count initializers (weights)
    for init in model.graph.initializer:
        try:
            a = numpy_helper.to_array(init)
            params += a.size
            memory += a.nbytes
        except Exception:
            pass
    
    # Count Constant nodes
    for nd in model.graph.node:
        if nd.op_type == 'Constant':
            for attr in nd.attribute:
                if attr.name == 'value' and attr.t and attr.t.ByteSize() > 0:
                    try:
                        a = numpy_helper.to_array(attr.t)
                        params += a.size
                        memory += a.nbytes
                    except Exception:
                        pass
                elif attr.name == 'value_floats':
                    params += len(attr.floats)
                    memory += len(attr.floats) * 4
                elif attr.name == 'value_ints':
                    params += len(attr.ints)
                    memory += len(attr.ints) * 8
        
        # Banned op check
        if nd.op_type.upper() in {op.upper() for op in BANNED_OPS}:
            print(f"WARNING: Banned op '{nd.op_type}' found in {path}")
            return None
    
    # Estimate intermediate tensor memory (node outputs that aren't 'output')
    n_intermediates = 0
    for nd in model.graph.node:
        for out_name in nd.output:
            if out_name and out_name != 'output':
                n_intermediates += 1
    
    # Conservative estimate: average intermediate ~20KB (mix of small and large tensors)
    memory += n_intermediates * 20000
    
    return int(memory), int(params)