|
| 1 | +import ast |
1 | 2 | import io |
| 3 | +import re |
2 | 4 | import sys |
3 | 5 | import traceback |
4 | 6 | import warnings |
| 7 | +from collections import Counter |
| 8 | +from keyword import iskeyword |
5 | 9 | from operator import itemgetter |
6 | | -from typing import Callable, Dict, Iterable, List, NoReturn, Optional, Tuple, Union |
| 10 | +from tempfile import NamedTemporaryFile |
| 11 | +from textwrap import indent |
| 12 | +from types import FunctionType |
| 13 | +from typing import Any, Callable, Dict, Iterable, List, NoReturn, Optional, Tuple, Union |
7 | 14 |
|
8 | 15 | import numpy as np |
9 | 16 |
|
10 | 17 | from aesara import utils |
11 | 18 | from aesara.configdefaults import config |
12 | | -from aesara.graph.basic import Apply, Constant |
| 19 | +from aesara.graph.basic import Apply, Constant, Variable |
13 | 20 | from aesara.graph.fg import FunctionGraph |
14 | 21 |
|
15 | 22 |
|
@@ -564,3 +571,139 @@ def wrapper(type, value, trace): |
564 | 571 |
|
565 | 572 |
|
566 | 573 | register_thunk_trace_excepthook() |
| 574 | + |
| 575 | + |
| 576 | +def fgraph_to_python( |
| 577 | + fgraph: FunctionGraph, |
| 578 | + op_conversion_fn: Callable, |
| 579 | + type_conversion_fn: Optional[Callable] = lambda x, **kwargs: x, |
| 580 | + order: Optional[List[Variable]] = None, |
| 581 | + input_storage: Optional[List[Any]] = None, |
| 582 | + output_storage: Optional[List[Any]] = None, |
| 583 | + storage_map: Optional[Dict[Variable, List[Any]]] = None, |
| 584 | + fgraph_name: str = "fgraph_to_python", |
| 585 | + global_env: Optional[Dict[Any, Any]] = None, |
| 586 | + local_env: Optional[Dict[Any, Any]] = None, |
| 587 | + **kwargs, |
| 588 | +) -> FunctionType: |
| 589 | + """Convert a ``FunctionGraph`` into a regular Python function. |
| 590 | +
|
| 591 | + Parameters |
| 592 | + ========== |
| 593 | + fgraph |
| 594 | + The ``FunctionGraph`` to convert. |
| 595 | + op_conversion_fn |
| 596 | + A callable used to convert nodes inside `fgraph` based on their ``Op`` |
| 597 | + types. It must have the signature ``(Op, **kwargs)``. One of the |
| 598 | + keyword arguments will be ``node``, which provides the ``Apply`` node. |
| 599 | + type_conversion_fn |
| 600 | + A callable used to convert the values in `storage_map`. |
| 601 | + order |
| 602 | + The ``order`` argument to ``map_storage``. |
| 603 | + input_storage |
| 604 | + The ``input_storage`` argument to ``map_storage``. |
| 605 | + output_storage |
| 606 | + The ``output_storage`` argument to ``map_storage``. |
| 607 | + storage_map |
| 608 | + The ``storage_map`` argument to ``map_storage``. |
| 609 | + fgraph_name |
| 610 | + The name used for the resulting function. |
| 611 | + global_env |
| 612 | + The global environment used when the function is constructed. |
| 613 | + The default is an empty ``dict``. |
| 614 | + local_env |
| 615 | + The local environment used when the function is constructed. |
| 616 | + The default is ``locals()``. |
| 617 | + **kwargs |
| 618 | + The remaining keywords are passed to `python_conversion_fn` |
| 619 | + """ |
| 620 | + |
| 621 | + if order is None: |
| 622 | + order = fgraph.toposort() |
| 623 | + input_storage, output_storage, storage_map = map_storage( |
| 624 | + fgraph, order, input_storage, output_storage, storage_map |
| 625 | + ) |
| 626 | + |
| 627 | + if global_env is None: |
| 628 | + global_env = {} |
| 629 | + |
| 630 | + def unique_name(x, names_counter=Counter([fgraph_name]), obj_to_names={}): |
| 631 | + if x in obj_to_names: |
| 632 | + return obj_to_names[x] |
| 633 | + |
| 634 | + if isinstance(x, Variable): |
| 635 | + name = re.sub("[^0-9a-zA-Z]+", "_", x.name) if x.name else "" |
| 636 | + name = ( |
| 637 | + name if (name.isidentifier() and not iskeyword(name)) else x.auto_name |
| 638 | + ) |
| 639 | + elif isinstance(x, FunctionType): |
| 640 | + name = x.__name__ |
| 641 | + else: |
| 642 | + name = type(x).__name__ |
| 643 | + |
| 644 | + name_suffix = names_counter.get(name, "") |
| 645 | + local_name = f"{name}{name_suffix}" |
| 646 | + |
| 647 | + names_counter.update((name,)) |
| 648 | + obj_to_names[x] = local_name |
| 649 | + |
| 650 | + return local_name |
| 651 | + |
| 652 | + body_assigns = [] |
| 653 | + for node in order: |
| 654 | + jax_func = op_conversion_fn(node.op, node=node, **kwargs) |
| 655 | + |
| 656 | + # Create a local alias with a unique name |
| 657 | + local_jax_func_name = unique_name(jax_func) |
| 658 | + global_env[local_jax_func_name] = jax_func |
| 659 | + |
| 660 | + node_input_names = [] |
| 661 | + for i in node.inputs: |
| 662 | + local_input_name = unique_name(i) |
| 663 | + if storage_map[i][0] is not None or isinstance(i, Constant): |
| 664 | + # Constants need to be assigned locally and referenced |
| 665 | + global_env[local_input_name] = type_conversion_fn( |
| 666 | + storage_map[i][0], node=None, **kwargs |
| 667 | + ) |
| 668 | + # TODO: We could attempt to use the storage arrays directly |
| 669 | + # E.g. `local_input_name = f"{local_input_name}[0]"` |
| 670 | + node_input_names.append(local_input_name) |
| 671 | + |
| 672 | + node_output_names = [unique_name(v) for v in node.outputs] |
| 673 | + |
| 674 | + body_assigns.append( |
| 675 | + f"{', '.join(node_output_names)} = {local_jax_func_name}({', '.join(node_input_names)})" |
| 676 | + ) |
| 677 | + |
| 678 | + fgraph_input_names = [unique_name(v) for v in fgraph.inputs] |
| 679 | + fgraph_output_names = [unique_name(v) for v in fgraph.outputs] |
| 680 | + joined_body_assigns = indent("\n".join(body_assigns), " ") |
| 681 | + |
| 682 | + if len(fgraph_output_names) == 1: |
| 683 | + fgraph_return_src = f"({fgraph_output_names[0]},)" |
| 684 | + else: |
| 685 | + fgraph_return_src = ", ".join(fgraph_output_names) |
| 686 | + |
| 687 | + fgraph_def_src = f""" |
| 688 | +def {fgraph_name}({", ".join(fgraph_input_names)}): |
| 689 | +{joined_body_assigns} |
| 690 | + return {fgraph_return_src} |
| 691 | + """ |
| 692 | + |
| 693 | + fgraph_def_ast = ast.parse(fgraph_def_src) |
| 694 | + |
| 695 | + # Create source code to be (at least temporarily) associated with the |
| 696 | + # compiled function (e.g. for easier debugging) |
| 697 | + with NamedTemporaryFile(delete=False) as f: |
| 698 | + filename = f.name |
| 699 | + f.write(fgraph_def_src.encode()) |
| 700 | + |
| 701 | + if local_env is None: |
| 702 | + local_env = locals() |
| 703 | + |
| 704 | + mod_code = compile(fgraph_def_ast, filename, mode="exec") |
| 705 | + exec(mod_code, global_env, local_env) |
| 706 | + |
| 707 | + fgraph_def = local_env[fgraph_name] |
| 708 | + |
| 709 | + return fgraph_def |
0 commit comments