Skip to content
This repository was archived by the owner on Nov 17, 2025. It is now read-only.

Commit 9859c79

Browse files
Generalize FunctionGraph conversion function with aesara.link.utils.fgraph_to_python
1 parent e691491 commit 9859c79

3 files changed

Lines changed: 162 additions & 99 deletions

File tree

aesara/link/jax/dispatch.py

Lines changed: 14 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,5 @@
1-
import ast
2-
import re
31
import warnings
4-
from collections import Counter
52
from functools import reduce, singledispatch
6-
from keyword import iskeyword
7-
from tempfile import NamedTemporaryFile
8-
from textwrap import indent
9-
from types import FunctionType
103
from warnings import warn
114

125
import jax
@@ -17,10 +10,9 @@
1710

1811
from aesara.compile.ops import DeepCopyOp, ViewOp
1912
from aesara.configdefaults import config
20-
from aesara.graph.basic import Constant, Variable
2113
from aesara.graph.fg import FunctionGraph
2214
from aesara.ifelse import IfElse
23-
from aesara.link.utils import map_storage
15+
from aesara.link.utils import fgraph_to_python
2416
from aesara.scalar.basic import Cast, Clip, Composite, Identity, ScalarOp, Second
2517
from aesara.scan.op import Scan
2618
from aesara.scan.utils import scan_args as ScanArgs
@@ -104,7 +96,7 @@
10496

10597

10698
@singledispatch
107-
def jax_typify(data, dtype):
99+
def jax_typify(data, dtype=None, **kwargs):
108100
"""Convert instances of Aesara `Type`s to JAX types."""
109101
if dtype is None:
110102
return data
@@ -113,12 +105,12 @@ def jax_typify(data, dtype):
113105

114106

115107
@jax_typify.register(np.ndarray)
116-
def jax_typify_ndarray(data, dtype):
108+
def jax_typify_ndarray(data, dtype=None, **kwargs):
117109
return jnp.array(data, dtype=dtype)
118110

119111

120112
@jax_typify.register(RandomState)
121-
def jax_typify_RandomState(state, dtype):
113+
def jax_typify_RandomState(state, **kwargs):
122114
state = state.get_state(legacy=False)
123115
state["bit_generator"] = numpy_bit_gens[state["bit_generator"]]
124116
return state
@@ -608,92 +600,18 @@ def jax_funcify_FunctionGraph(
608600
storage_map=None,
609601
**kwargs,
610602
):
611-
612-
if order is None:
613-
order = fgraph.toposort()
614-
input_storage, output_storage, storage_map = map_storage(
615-
fgraph, order, input_storage, output_storage, storage_map
603+
return fgraph_to_python(
604+
fgraph,
605+
jax_funcify,
606+
jax_typify,
607+
order,
608+
input_storage,
609+
output_storage,
610+
storage_map,
611+
fgraph_name="jax_funcified_fgraph",
612+
**kwargs,
616613
)
617614

618-
global_env = {}
619-
fgraph_name = "jax_funcified_fgraph"
620-
621-
def unique_name(x, names_counter=Counter([fgraph_name]), obj_to_names={}):
622-
if x in obj_to_names:
623-
return obj_to_names[x]
624-
625-
if isinstance(x, Variable):
626-
name = re.sub("[^0-9a-zA-Z]+", "_", x.name) if x.name else ""
627-
name = (
628-
name if (name.isidentifier() and not iskeyword(name)) else x.auto_name
629-
)
630-
elif isinstance(x, FunctionType):
631-
name = x.__name__
632-
else:
633-
name = type(x).__name__
634-
635-
name_suffix = names_counter.get(name, "")
636-
local_name = f"{name}{name_suffix}"
637-
638-
names_counter.update((name,))
639-
obj_to_names[x] = local_name
640-
641-
return local_name
642-
643-
body_assigns = []
644-
for node in order:
645-
jax_func = jax_funcify(node.op, node=node, **kwargs)
646-
647-
# Create a local alias with a unique name
648-
local_jax_func_name = unique_name(jax_func)
649-
global_env[local_jax_func_name] = jax_func
650-
651-
node_input_names = []
652-
for i in node.inputs:
653-
local_input_name = unique_name(i)
654-
if storage_map[i][0] is not None or isinstance(i, Constant):
655-
# Constants need to be assigned locally and referenced
656-
global_env[local_input_name] = jax_typify(storage_map[i][0], None)
657-
# TODO: We could attempt to use the storage arrays directly
658-
# E.g. `local_input_name = f"{local_input_name}[0]"`
659-
node_input_names.append(local_input_name)
660-
661-
node_output_names = [unique_name(v) for v in node.outputs]
662-
663-
body_assigns.append(
664-
f"{', '.join(node_output_names)} = {local_jax_func_name}({', '.join(node_input_names)})"
665-
)
666-
667-
fgraph_input_names = [unique_name(v) for v in fgraph.inputs]
668-
fgraph_output_names = [unique_name(v) for v in fgraph.outputs]
669-
joined_body_assigns = indent("\n".join(body_assigns), " ")
670-
671-
if len(fgraph_output_names) == 1:
672-
fgraph_return_src = f"({fgraph_output_names[0]},)"
673-
else:
674-
fgraph_return_src = ", ".join(fgraph_output_names)
675-
676-
fgraph_def_src = f"""
677-
def {fgraph_name}({", ".join(fgraph_input_names)}):
678-
{joined_body_assigns}
679-
return {fgraph_return_src}
680-
"""
681-
682-
fgraph_def_ast = ast.parse(fgraph_def_src)
683-
684-
# Create source code to be (at least temporarily) associated with the
685-
# compiled function (e.g. for easier debugging)
686-
with NamedTemporaryFile(delete=False) as f:
687-
filename = f.name
688-
f.write(fgraph_def_src.encode())
689-
690-
mod_code = compile(fgraph_def_ast, filename, mode="exec")
691-
exec(mod_code, global_env, locals())
692-
693-
fgraph_def = locals()[fgraph_name]
694-
695-
return fgraph_def
696-
697615

698616
@jax_funcify.register(CAReduce)
699617
def jax_funcify_CAReduce(op, **kwargs):

aesara/link/jax/linker.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,9 @@ def create_jax_thunks(
6969
for n in self.fgraph.inputs:
7070
sinput = storage_map[n]
7171
if isinstance(sinput[0], RandomState):
72-
new_value = jax_typify(sinput[0], getattr(sinput[0], "dtype", None))
72+
new_value = jax_typify(
73+
sinput[0], dtype=getattr(sinput[0], "dtype", None)
74+
)
7375
# We need to remove the reference-based connection to the
7476
# original `RandomState`/shared variable's storage, because
7577
# subsequent attempts to use the same shared variable within

aesara/link/utils.py

Lines changed: 145 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
import ast
12
import io
3+
import re
24
import sys
35
import traceback
46
import warnings
7+
from collections import Counter
8+
from keyword import iskeyword
59
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
714

815
import numpy as np
916

1017
from aesara import utils
1118
from aesara.configdefaults import config
12-
from aesara.graph.basic import Apply, Constant
19+
from aesara.graph.basic import Apply, Constant, Variable
1320
from aesara.graph.fg import FunctionGraph
1421

1522

@@ -564,3 +571,139 @@ def wrapper(type, value, trace):
564571

565572

566573
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

Comments
 (0)