File: decorator.py

package info (click to toggle)
python-einx 0.3.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,112 kB
  • sloc: python: 11,619; makefile: 13
file content (245 lines) | stat: -rw-r--r-- 7,819 bytes parent folder | download
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
import functools
import os
import einx
import threading
import frozendict
import inspect
import sys
from functools import partial
import numpy as np
from collections import defaultdict
from .compile import CompiledFunction
from .tracer import *


def _freeze(x):
    if isinstance(x, np.ndarray):
        return tuple(x.tolist())
    elif isinstance(x, (list, tuple)):
        return tuple(_freeze(x) for x in x)
    elif isinstance(x, dict):
        return frozendict.frozendict({k: _freeze(v) for k, v in x.items()})
    else:
        return x


def freeze(func):
    @functools.wraps(func)
    def func_frozen(*args, **kwargs):
        args = [_freeze(a) for a in args]
        kwargs = {k: _freeze(v) for k, v in kwargs.items()}
        return func(*args, **kwargs)

    return func_frozen


traced_functions_decorators = []
traced_functions = []
traced_functions_lock = threading.Lock()

thread_local = threading.local()
thread_local.warn = True


def _with_retrace_warning(func):
    warn_on_retrace_num = int(os.environ.get("EINX_WARN_ON_RETRACE", 0))

    if warn_on_retrace_num > 0:
        cache_failures = defaultdict(lambda: 0)

        @functools.wraps(func)
        def func_with_warn(*args, **kwargs):
            has_warned = False
            if warn_on_retrace_num > 0:
                stack = inspect.stack()
                # Exclude frames called from this file
                last_index = 0
                for i, frame in enumerate(stack):
                    if frame.filename == __file__:
                        last_index = i
                stack = stack[last_index + 1 :]

                if len(stack) > 0:
                    # Generate string description of call stack
                    trace = ""
                    for frame in reversed(stack):
                        trace += (
                            f'File "{frame.filename}", line {frame.lineno}, in {frame.function}\n'
                        )
                        if frame.code_context is not None:
                            trace += f"  {frame.code_context[0].strip()}\n"
                    cache_failures[trace] += 1
                    if thread_local.warn and cache_failures[trace] == warn_on_retrace_num:
                        # Print warning
                        has_warned = True
                        print(
                            f"WARNING (einx): The following call stack has resulted in "
                            f"{warn_on_retrace_num} retraces of an einx function.\n"
                            f"A retrace happens when the function is called with "
                            "different signatures of input arguments.\n"
                            f"Call stack (most recent call last):\n"
                            f"{trace}"
                        )

            # Don't warn in inner functions that also use lru_cache
            if has_warned:
                thread_local.warn = False
                result = func(*args, **kwargs)
                thread_local.warn = True
            else:
                result = func(*args, **kwargs)
            return result

        return func_with_warn
    else:
        return func


def lru_cache(func):
    func = _with_retrace_warning(func)

    max_cache_size = int(os.environ.get("EINX_CACHE_SIZE", -1))
    if max_cache_size > 0:
        func = functools.lru_cache(maxsize=max_cache_size if max_cache_size > 0 else None)(func)
    elif max_cache_size < 0:
        if "cache" in vars(functools):
            func = functools.cache(func)
        else:
            func = functools.lru_cache(maxsize=None)(func)
    func = freeze(func)

    return func


_thread_local = threading.local()


def _get_trace_stack():
    if not hasattr(_thread_local, "stack"):
        _thread_local.stack = []
    return _thread_local.stack


class _trace_context:
    def __init__(self, backend):
        self.backend = backend

    def __enter__(self):
        _get_trace_stack().append(self)

    def __exit__(self, *args):
        assert id(_get_trace_stack()[-1]) == id(self)
        _get_trace_stack().pop()


def _is_tracing():
    return len(_get_trace_stack()) > 0


trace_all = lambda t, c: lambda *args, **kwargs: c(
    *[t(arg) for arg in args], **{k: t(v) for k, v in kwargs.items()}
)
trace_none = lambda t, c: lambda *args, **kwargs: c(args, kwargs)


def jit(func=None, trace=trace_all):
    if func is None:
        return partial(jit, trace=trace)

    @lru_cache
    def construct_graph(args, kwargs, backend):
        with _trace_context(backend):
            # Replace input keys with tracers and retrieve list of traced arguments
            virtual_arg = einx.tracer.Tracer()
            input_tracers, (args, kwargs) = einx.tracer.input.key_to_tracer(
                (args, kwargs), backend, virtual_arg
            )

            # Trace function
            output_tracer = func(*args, backend=backend, **kwargs)

            # Create function that takes only traced arguments as input
            function = TracableFunction(
                args=input_tracers,
                output=output_tracer,
                name=backend.function_name,
                virtual_args=[virtual_arg],
            )
            for decorator in backend.decorators:
                function = decorator(function)

            # Convert to graph
            return CompiledFunction(function)

    def find_backend_and_construct_graph(args, kwargs, traced_input_values, backend):
        # Determine backend
        if backend is None:
            backend = einx.backend.get_default()
            if backend is None:
                backend = einx.backend.get(traced_input_values)
        elif isinstance(backend, str):
            backend = einx.backend.get(backend)

        # Construct graph/ retrieve from cache
        graph = construct_graph(args=args, kwargs=kwargs, backend=backend)

        return graph

    has_decorated = False

    @functools.wraps(func)
    def func_jit(*args, backend=None, graph=False, **kwargs):
        if _is_tracing():
            assert not graph
            if backend is None:
                backend = _get_trace_stack()[-1].backend
            elif backend != _get_trace_stack()[-1].backend:
                raise ValueError("Cannot change backend during tracing")

            return func(*args, backend=backend, **kwargs)

        return_graph = graph

        # Replace concrete values with tracers
        traced_input_values = []

        def new_input(x):
            value, key = einx.tracer.input.concrete_to_value_and_key(x)
            if not value is None:
                traced_input_values.append(value)
            return key

        args, kwargs = trace(new_input, lambda *args, **kwargs: (args, kwargs))(*args, **kwargs)

        # Disable torch.compile for graph construction (if torch is imported)
        nonlocal has_decorated, find_backend_and_construct_graph
        if not has_decorated and "torch" in sys.modules:
            import torch._dynamo as _dynamo

            find_backend_and_construct_graph = _dynamo.disable(find_backend_and_construct_graph)
            has_decorated = True

        graph = find_backend_and_construct_graph(args, kwargs, traced_input_values, backend)

        # Execute/ return graph
        if return_graph:
            return graph
        else:
            return graph(*traced_input_values)

    with traced_functions_lock:
        traced_functions.append(func_jit)
        for decorator in traced_functions_decorators:
            decorator(func_jit)

    return func_jit


def decorate_traced_functions(decorator):
    with traced_functions_lock:
        for func in traced_functions:
            decorator(func)
        traced_functions_decorators.append(decorator)


jit.decorate_traced_functions = decorate_traced_functions