Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 62 additions & 26 deletions utils/modular_model_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from create_dependency_mapping import find_priority_list
from libcst import ClassDef, CSTVisitor
from libcst import matchers as m
from libcst.metadata import MetadataWrapper, ParentNodeProvider, PositionProvider, ScopeProvider
from libcst.metadata import MetadataWrapper, ScopeProvider
from modular_integrations import (
EXCLUDED_EXTERNAL_FILES,
AbsoluteImportTransformer,
Expand All @@ -49,16 +49,30 @@
# 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
"""

_MODULE_SOURCE_CACHE = {}

def get_module_source_from_name(module_name: str) -> str:
# Extract the source code from the module name

def get_module_source_and_tree_from_name(module_name: str) -> tuple[str, cst.Module]:
spec = importlib.util.find_spec(module_name)
if spec is None or spec.origin is None:
raise ValueError(f"Cannot open file associated with {module_name} module.")

with open(spec.origin, "r", encoding="utf-8") as file:
file_path = spec.origin
cache_key = (module_name, file_path)
mtime_ns = os.stat(file_path).st_mtime_ns
cached = _MODULE_SOURCE_CACHE.get(cache_key)
if cached is not None and cached[0] == mtime_ns:
return cached[1], cached[2]
Comment on lines +62 to +65

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we cache the time here?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not the time it's the file's st_mtime_ns that we use to determine if it was modified. It's used for a small performance gain and also the profiler.

The gains for the execution side are minimal, if we remove the profiler we can probably rip off this part as well

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

15% gain on reads


with open(file_path, "r", encoding="utf-8") as file:
source_code = file.read()
return source_code
tree = cst.parse_module(source_code)
_MODULE_SOURCE_CACHE[cache_key] = (mtime_ns, source_code, tree)
return source_code, tree


def get_module_source_from_name(module_name: str) -> str:
return get_module_source_and_tree_from_name(module_name)[0]


def preserve_case_replace(text, patterns: dict, default_name: str):
Expand Down Expand Up @@ -534,8 +548,6 @@ class ModuleMapper(CSTVisitor, ABC):
modeling files that will be visited.
"""

METADATA_DEPENDENCIES = (ParentNodeProvider, PositionProvider)

def __init__(self, python_module: cst.Module):
# fmt: off
self.python_module: cst.Module = python_module # original cst.Module being visited
Expand All @@ -547,12 +559,35 @@ def __init__(self, python_module: cst.Module):
self.current_function = None # this keeps track of the current module-scope function
self.current_class = None # this keeps track of the current module-scope class
self.current_assignment = None # this keeps track of the current module-scope assignment
self._suite_depth = 0 # tracks whether we are inside an indented/simple statement suite
self._node_order = {} # source order of recorded top-level nodes
self._next_node_order = 0
# this keeps track of objects imported from modeling files (`from .configuration import Config`) -> `Config` should not be a dependency
self.objects_imported_from_modeling = set()
# regex pattern joining every possible file type
self.match_patterns = "|".join(ALL_FILE_TYPES)
# fmt: on

def _is_direct_module_child(self) -> bool:
return self._suite_depth == 0

def _record_node_order(self, node_name: str) -> None:
if node_name not in self._node_order:
self._node_order[node_name] = self._next_node_order
self._next_node_order += 1

def visit_IndentedBlock(self, node):
self._suite_depth += 1

def leave_IndentedBlock(self, node):
self._suite_depth -= 1

def visit_SimpleStatementSuite(self, node):
self._suite_depth += 1

def leave_SimpleStatementSuite(self, node):
self._suite_depth -= 1

def visit_ImportFrom(self, node):
"""This keeps track of objects imported from neighbor modeling files (e.g. in `modeling_xxx.py, we have
`from .configuration_xxx import Config`, then `Config` should be recorded as it is not a dependency that needs
Expand All @@ -573,19 +608,20 @@ def visit_SimpleStatementLine(self, node):
Global Assigns like `GEMMA_INPUT_DOCSTRING = 'THIS IS THE INPUT'` and all import statements
are extracted and saved in their corresponding dict. They are then used when updating dependency mappings.
"""
parent_node = self.get_metadata(cst.metadata.ParentNodeProvider, node)
simple_top_level_assign_structure = m.SimpleStatementLine(
body=[m.Assign(targets=[m.AssignTarget(target=m.Name())])]
)
simple_top_level_variable_indexing = m.SimpleStatementLine(
body=[m.Assign(targets=[m.AssignTarget(target=m.Subscript(value=m.Name()) | m.Attribute(value=m.Name()))])]
)

if m.matches(parent_node, m.Module()):
is_module_level = self._is_direct_module_child()
if is_module_level:
if m.matches(node, simple_top_level_assign_structure):
left_hand_side = node.body[0].targets[0].target.value
self.current_assignment = left_hand_side
self.assignments[left_hand_side] = node
self._record_node_order(left_hand_side)
# This corresponds to a global variable being indexed or having an attribute look-up
elif m.matches(node, simple_top_level_variable_indexing):
indexed_variable = node.body[0].targets[0].target.value.value
Expand All @@ -595,6 +631,7 @@ def visit_SimpleStatementLine(self, node):
node_name = self.python_module.code_for_node(node)
self.assignments[node_name] = node
self.object_dependency_mapping[indexed_variable].add(node_name)
self._record_node_order(node_name)
elif m.matches(node, m.SimpleStatementLine(body=[m.Import() | m.ImportFrom()])):
self.imports.append(node)

Expand All @@ -604,14 +641,15 @@ def leave_SimpleStatementLine(self, node):
self.current_assignment = None

def visit_FunctionDef(self, node):
parent_node = self.get_metadata(cst.metadata.ParentNodeProvider, node)
if m.matches(parent_node, m.Module()):
is_module_level = self._is_direct_module_child()
if is_module_level:
self.current_function = node.name.value
self.functions[node.name.value] = node
self._record_node_order(node.name.value)

def leave_FunctionDef(self, node):
parent_node = self.get_metadata(cst.metadata.ParentNodeProvider, node)
if m.matches(parent_node, m.Module()):
is_module_level = self._is_direct_module_child()
if is_module_level:
self.current_function = None

def visit_If(self, node):
Expand All @@ -624,6 +662,7 @@ def visit_If(self, node):
def visit_ClassDef(self, node: ClassDef) -> None:
"""Record class nodes to create their dependencies at the end."""
self.classes[node.name.value] = node
self._record_node_order(node.name.value)
self.current_class = node.name.value

def leave_ClassDef(self, node):
Expand All @@ -638,16 +677,14 @@ def visit_Name(self, node: cst.Call):

def leave_Module(self, node):
"""When leaving the module, we store the position of each global scoped node to allow sorting the dependencies
based on their position in the code later. We use the PositionProvider metadata wrapper for this.
based on their position in the code later.
We also make sure to update `self.object_dependency_mapping` so that it contains only names recorded in
`self.global_nodes`.
"""
# assign all nodes
self.global_nodes = {**self.assignments, **self.classes, **self.functions}
# now sort the class dependency_mapping based on the position of the nodes
self.start_lines = {}
for id, node in self.global_nodes.items():
self.start_lines[id] = self.get_metadata(cst.metadata.PositionProvider, node).start.line
self.start_lines = {node_name: self._node_order[node_name] for node_name in self.global_nodes}

def _restrict_dependencies_to_known_entities(self):
"""Since we added every Name as part of `self.object_dependency_mapping`, we need to remove those that
Expand Down Expand Up @@ -855,9 +892,8 @@ def merge_modular_dependencies(self, classes, functions, assignments, object_map
def visit_and_merge_dependencies(
cls, module: cst.Module, classes, functions, assignments, object_mapping, start_lines
) -> "ModelFileMapper":
wrapper = MetadataWrapper(module)
mapper = cls(module)
wrapper.visit(mapper)
module.visit(mapper)
# Merge dependencies
mapper.merge_modular_dependencies(classes, functions, assignments, object_mapping, start_lines)
# Create the class dependencies graph
Expand Down Expand Up @@ -1159,7 +1195,7 @@ def get_needed_imports(body: dict[str, dict], all_imports: list[cst.CSTNode]) ->
Note: we need to use `isinstance` on scope assignments, m.matches apparently does not work here yet!
"""
new_body = [k[1]["node"] for k in sorted(body.items(), key=lambda x: x[1]["insert_idx"])]
wrapper = MetadataWrapper(cst.Module(body=all_imports + new_body))
wrapper = MetadataWrapper(cst.Module(body=all_imports + new_body), unsafe_skip_copy=True)
scopes = set(wrapper.resolve(ScopeProvider).values())
unused_imports = set()
import_ref_count = defaultdict(lambda: 0)
Expand Down Expand Up @@ -1414,12 +1450,11 @@ def visit_ImportFrom(self, node: cst.ImportFrom) -> None:
if not import_module.startswith("transformers"):
import_module = "transformers." + import_module
try:
source_code = get_module_source_from_name(import_module)
_, tree = get_module_source_and_tree_from_name(import_module)
except ModuleNotFoundError as e:
raise ModuleNotFoundError(
f"Failed to visit import from for: {self.python_module.code_for_node(node)}. Tried to import {import_module} but failed."
) from e
tree = cst.parse_module(source_code)
self.model_specific_modules[import_module] = tree
imported_object = self.python_module.code_for_node(imported_.name)
self.model_specific_imported_objects[imported_object] = import_module
Expand All @@ -1433,15 +1468,15 @@ def visit_SimpleStatementLine(self, node):
"""If we visit an import statement not previously visited, record it. If we visit a module-scope assignment,
simply record it or, if it is `__all__`, split it between files where we should dispatch it.
"""
parent_node = self.get_metadata(cst.metadata.ParentNodeProvider, node)
simple_top_level_assign_structure = m.SimpleStatementLine(
body=[m.Assign(targets=[m.AssignTarget(target=m.Name())])]
)
simple_top_level_variable_indexing = m.SimpleStatementLine(
body=[m.Assign(targets=[m.AssignTarget(target=m.Subscript(value=m.Name()) | m.Attribute(value=m.Name()))])]
)

if m.matches(parent_node, m.Module()):
is_module_level = self._is_direct_module_child()
if is_module_level:
if m.matches(node, m.SimpleStatementLine(body=[m.Import()])):
self.imports.append(node)
elif m.matches(node, m.SimpleStatementLine(body=[m.ImportFrom()])):
Expand All @@ -1465,6 +1500,7 @@ def visit_SimpleStatementLine(self, node):
else:
self.current_assignment = assigned_variable
self.assignments[assigned_variable] = node
self._record_node_order(assigned_variable)
# This corresponds to a global variable being indexed or having an attribute look-up
elif m.matches(node, simple_top_level_variable_indexing):
indexed_variable = node.body[0].targets[0].target.value.value
Expand All @@ -1474,6 +1510,7 @@ def visit_SimpleStatementLine(self, node):
node_name = self.python_module.code_for_node(node)
self.assignments[node_name] = node
self.object_dependency_mapping[indexed_variable].add(node_name)
self._record_node_order(node_name)

def leave_Module(self, node):
"""When we leave the modular file, we do the following in order:
Expand Down Expand Up @@ -1948,9 +1985,8 @@ def convert_modular_file(modular_file: str, source_library: str | None = "transf
if source_library != "transformers":
module = module.visit(AbsoluteImportTransformer(relative_path, source_library))

wrapper = MetadataWrapper(module)
cst_transformers = ModularFileMapper(module, model_name, source_library)
wrapper.visit(cst_transformers)
module.visit(cst_transformers)
for file, module in create_modules(
cst_transformers, file_path=relative_path, package_name=source_library
).items():
Expand Down
Loading