diff --git a/ChangeLog.md b/ChangeLog.md index 59c96761..55ed3b3c 100644 --- a/ChangeLog.md +++ b/ChangeLog.md @@ -5,6 +5,17 @@ SPDX-License-Identifier: Apache-2.0 # Changelog +## Unreleased + +- feat (breaking): `inspect` measures module depth in executed modules. + Containers such as `nn.ModuleList` and modules that never run no longer add a level. + `min_depth` is renamed to `depth`. + `depth` keeps shallower executed modules without executed children, so a cut covers the whole model. + `max_depth` is removed, the traversal limit follows from `depth`. + A diffusers pipeline that used `min_depth=1` to reach its components now uses `depth=0`. + `inspect` warns when the walk stops before any module is registered. + `warn_if_no_modules=False` turns that warning off. + ## 0.4.0 This is the first release of **Torch Tweak**. Torch Tweak is focused on Intel GPU (XPU) support. diff --git a/docs/guides/inspect.md b/docs/guides/inspect.md index 58f85008..f88bd259 100644 --- a/docs/guides/inspect.md +++ b/docs/guides/inspect.md @@ -42,8 +42,8 @@ modules_info = tt.inspect( inference_function=None, # Optional custom inference function number_of_iterations=10, # Iterations for profiling warmup_iterations=5, # Warmup iterations - min_depth=0, # Minimum depth for module search - max_depth=5, # Maximum depth for module search + depth=0, # Depth at which the model is cut into modules + warn_if_no_modules=True, # Warn when the walk stops before any module is registered ) ``` @@ -133,24 +133,48 @@ modules_info = tt.inspect( ) ``` -#### min_depth (Default: 0) +#### Module depth -Minimum depth level for module discovery. Increase if root-level modules don't work: +The depth of a module is the number of executed modules above it. +Depth 0 holds the outermost executed modules. +That is the model itself, or the executed components of a pipeline object. +Containers such as `nn.ModuleList`, lists, and dicts do not add a level. +Modules whose `forward` never ran do not add a level either. + +For a model whose `forward` loops over `self.layers` (an `nn.ModuleList`) and then calls `self.head`: + +| Depth | Modules | +|---|---| +| 0 | `model` | +| 1 | `layers.0`, `layers.1`, `head` | +| 2 | `layers.0.mlp`, `layers.1.mlp` | + +#### depth (Default: 0) + +Depth at which the model is cut into modules for tuning. +Every executed module at `depth` is returned. +A shallower executed module without executed children is returned too, so no part of the model is left out. +In the example above, `depth=2` returns `layers.0.mlp`, `layers.1.mlp`, and `head`. + +Increase it when the outermost modules cannot be tuned: ```python -# Start from root level (default) -modules_info = tt.inspect(model, input_data, min_depth=0) +# Cut at the outermost executed modules (default) +modules_info = tt.inspect(model, input_data, depth=0) -# Skip root, inspect children -modules_info = tt.inspect(model, input_data, min_depth=1) -modules_info = tt.inspect(model, input_data, min_depth=2) +# Cut one level deeper +modules_info = tt.inspect(model, input_data, depth=1) ``` -#### max_depth (Default: 5) +#### warn_if_no_modules (Default: True) -If a nested (child) module has a larger depth than `max_depth` it will be skipped from inspection. +Warn when the walk stops before any module is registered. +The message says how far the walk went and to increase `depth`. +Pass `False` to silence that warning. -Both `min_depth` and `max_depth` narrow the inspection search to a reasonable range. +```python +modules_info = tt.inspect(model, input_data, warn_if_no_modules=False) +``` ## Working with InspectedModulesInfo @@ -211,8 +235,8 @@ transformer_modules = [m for m in modules_info.get_modules() if "transformer" in ### Issue: No modules found ```python -# Solution: Increase min_depth or check object structure -modules_info = tt.inspect(model, input_data, min_depth=1) +# Solution: Change depth or check object structure +modules_info = tt.inspect(model, input_data, depth=1) ``` ## Summary diff --git a/examples/E5Large/e5large/tune.py b/examples/E5Large/e5large/tune.py index c8ef63b5..32f92bff 100644 --- a/examples/E5Large/e5large/tune.py +++ b/examples/E5Large/e5large/tune.py @@ -80,10 +80,10 @@ def call_wrapper(*args, **kwargs): device="xpu", ) - # NOTE: without min_depth=2, inspector finds a wrapper module `pipeline._modules["0"]` and fails with incorrect input in tunning + # NOTE: without depth=2, inspector finds a wrapper module `pipeline._modules["0"]` and fails with incorrect input in tuning # with error: RuntimeError: Only Tensors created explicitly by the user (graph leaves) support the deepcopy protocol at the moment. @ torch/_tensor.py", line 136, in __deepcopy__ # during make_batch and deepcopy of arguments with all kwags arguments passed as dict in args[0] - inspected_modules_info = inspect(model, input_texts, inference_function=call_wrapper, min_depth=2) + inspected_modules_info = inspect(model, input_texts, inference_function=call_wrapper, depth=2) inspected_modules_info.describe() # NOTE: workaround for SentenceTransformer to work with tuned model see below first dirty hack diff --git a/examples/ParakeetCTC/parakeet_ctc/tune.py b/examples/ParakeetCTC/parakeet_ctc/tune.py index 910fb04b..dc8ad0c5 100644 --- a/examples/ParakeetCTC/parakeet_ctc/tune.py +++ b/examples/ParakeetCTC/parakeet_ctc/tune.py @@ -57,7 +57,7 @@ def call_wrapper(*args, **kwargs): input_data = [{"audio": str(audio_path)}] - inspected_modules_info = inspect(pipeline, input_data, inference_function=call_wrapper, min_depth=1) + inspected_modules_info = inspect(pipeline, input_data, inference_function=call_wrapper, depth=1) inspected_modules_info.describe() modules = inspected_modules_info.get_modules(min_execution_percentage=0.01) diff --git a/examples/ParakeetRNNT/parakeet_rnnt/tune.py b/examples/ParakeetRNNT/parakeet_rnnt/tune.py index 10a2b191..16d2f4f8 100644 --- a/examples/ParakeetRNNT/parakeet_rnnt/tune.py +++ b/examples/ParakeetRNNT/parakeet_rnnt/tune.py @@ -80,7 +80,7 @@ def call_wrapper(*args, **kwargs): call_wrapper(audio=str(audio_path)) logger.info("Inspecting model...") - inspected_modules_info = inspect(pipeline, input_data, inference_function=call_wrapper, min_depth=1) + inspected_modules_info = inspect(pipeline, input_data, inference_function=call_wrapper, depth=1) inspected_modules_info.describe() modules = inspected_modules_info.get_modules(min_execution_percentage=0.01) diff --git a/examples/StableDiffusion/stable_diffusion/benchmark_hybrid.py b/examples/StableDiffusion/stable_diffusion/benchmark_hybrid.py index 3a313fcf..f23b1f89 100644 --- a/examples/StableDiffusion/stable_diffusion/benchmark_hybrid.py +++ b/examples/StableDiffusion/stable_diffusion/benchmark_hybrid.py @@ -101,7 +101,6 @@ def call_for_inspect(*args, **kwargs): inference_function=call_for_inspect, number_of_iterations=2, warmup_iterations=1, - min_depth=1, ) modules = modules_info.get_modules(min_execution_percentage=min_execution_pct) logger.info("Wrapping %d modules (min_execution_percentage=%.2f)", len(modules), min_execution_pct) diff --git a/tests/unit/torch/inspecting/test_inspecting.py b/tests/unit/torch/inspecting/test_inspecting.py index 15350d06..d40382bf 100644 --- a/tests/unit/torch/inspecting/test_inspecting.py +++ b/tests/unit/torch/inspecting/test_inspecting.py @@ -357,6 +357,55 @@ def test_get_modules_after_inspection_with_min_execution_percentage(custom_objec assert executed_modules[0].execution_count == TEST_NUMBER_OF_ITERATIONS * custom_object.num_iterations +@pytest.fixture +def module_past_default_path_limit(): + """A linear at path depth 7. Default depth=0 stops at path depth 6.""" + return { + "nested": { + "nested": { + "nested": { + "nested": { + "nested": { + "nested": {"module": nn.Linear(4, 4)}, + }, + }, + }, + }, + }, + } + + +def _echo(tensor): + return tensor + + +def test_inspect_warns_when_the_walk_stops_before_any_module(module_past_default_path_limit, capsys): + modules_info = inspect(module_past_default_path_limit, torch.zeros(1, 4), inference_function=_echo) + + assert modules_info.get_modules() == [] + assert ( + "No modules found. The walk stopped at path depth 6, calculated from depth=0. Increase depth to walk further." + ) in capsys.readouterr().out + + +def test_inspect_warning_can_be_disabled(module_past_default_path_limit, capsys): + modules_info = inspect( + module_past_default_path_limit, + torch.zeros(1, 4), + inference_function=_echo, + warn_if_no_modules=False, + ) + + assert modules_info.get_modules() == [] + assert "No modules found" not in capsys.readouterr().out + + +def test_inspect_does_not_warn_when_a_module_is_found(simple_model, sample_dataset, capsys): + inspect(simple_model, sample_dataset, number_of_iterations=1, warmup_iterations=1) + + assert "No modules found" not in capsys.readouterr().out + + def test_get_modules_after_inspection_with_limit(custom_object, sample_dataset): """Test getting executed modules with a limit.""" # When inspecting custom object with various attributes diff --git a/tests/unit/torch/inspecting/test_module_inspector.py b/tests/unit/torch/inspecting/test_module_inspector.py index 8b330e99..0639d460 100644 --- a/tests/unit/torch/inspecting/test_module_inspector.py +++ b/tests/unit/torch/inspecting/test_module_inspector.py @@ -101,7 +101,7 @@ def test_initialization(): assert inspector._module_info == {} assert inspector._original_forward == {} assert inspector._inspected_objects == set() - assert inspector._max_recursion_depth == 5 + assert inspector._depth == 0 def test_reset(simple_model): @@ -149,15 +149,6 @@ def test_inspect_nested_model(nested_model): assert root_module.name == "Sequential" -def test_inspect_nested_model_max_depth(nested_model): - """Test inspecting a nested model.""" - inspector = ModuleInspector(max_depth=1) - inspector.inspect(nested_model) - - # Check that all modules were registered - assert len(inspector._module_info) == 4 # Sequential, Linear, NestedModule, Linear - - def test_inspect_model_with_dict(model_with_dict): """Test inspecting a model with dictionary attributes.""" inspector = ModuleInspector() @@ -273,20 +264,62 @@ def test_inspect_list_with_non_module_items(): assert len(inspector._module_info) == 1 -def test_inspect_recursion_depth_limit(): +@pytest.fixture +def deep_linears(): + """Linears placed along one nested path.""" + m1 = nn.Linear(10, 10) + m2 = nn.Linear(10, 10) + m3 = nn.Linear(10, 10) + m4 = nn.Linear(10, 10) + m5 = nn.Linear(10, 10) + m6 = nn.Linear(10, 10) + return m1, m2, m3, m4, m5, m6 + + +@pytest.fixture +def deep_dict(deep_linears): + """One nested dict. Comments are `path_depth` of each linear.""" + m1, m2, m3, m4, m5, m6 = deep_linears + return { + "nested": { # path_depth 1 + "module": m1, # path_depth 2 + "nested": { # path_depth 2 + "module": m2, # path_depth 3 + "nested": { # path_depth 3 + "module": m3, # path_depth 4 + "nested": { # path_depth 4 + "module": m4, # path_depth 5 + "nested": { # path_depth 5 + "module": m5, # path_depth 6 + "nested": { # path_depth 6 + "module": m6, # path_depth 7 + }, + }, + }, + }, + }, + }, + } + + +def test_inspect_recursion_depth_limit(deep_dict, deep_linears): """Test that recursion depth is properly limited.""" - # Create a deeply nested structure - deep_dict = {} - current = deep_dict - for _ in range(10): # More than max_recursion_depth - current["nested"] = {"module": nn.Linear(10, 10)} - current = current["nested"] - - inspector = ModuleInspector(max_depth=4) + m1, m2, m3, m4, m5, _m6 = deep_linears + inspector = ModuleInspector() inspector.inspect(deep_dict) - # Should still work without infinite recursion - assert len(inspector._module_info) == 3 # dict is not included as it is not a nn.Module + assert inspector._max_path_depth == 6 + assert {id(module) for module in inspector._module_info} == {id(m1), id(m2), id(m3), id(m4), id(m5)} + + +def test_get_modules_is_empty_when_module_is_past_max_path_depth(deep_dict, deep_linears): + """A module past the calculated path limit is not returned.""" + m6 = deep_linears[5] + inspector = ModuleInspector() + inspector.inspect(deep_dict) + m6(torch.randn(1, 10)) + + assert inspector.get_modules() == [] def test_inspect_duplicate_objects(): @@ -343,6 +376,96 @@ def test_get_modules_with_dict_model(model_with_dict): assert all(module.forward_called for module in executed_modules) +class _Block(nn.Module): + def __init__(self): + super().__init__() + self.mlp = nn.Linear(8, 8) + + def forward(self, x): + return self.mlp(x) + + +class _EncoderWithHead(nn.Module): + def __init__(self): + super().__init__() + self.layers = nn.ModuleList([_Block(), _Block()]) + self.head = nn.Linear(8, 8) + + def forward(self, x): + for layer in self.layers: + x = layer(x) + return self.head(x) + + +def _executed_encoder_inspector(depth: int = 0) -> ModuleInspector: + model = _EncoderWithHead() + inspector = ModuleInspector(depth=depth) + inspector.inspect(model) + model(torch.randn(1, 8)) + return inspector + + +def test_depth_counts_executed_modules_only(): + """`nn.ModuleList` is never executed, so it does not add a depth level.""" + depths = {m.object_path: m.depth for m in _executed_encoder_inspector()._module_info.values() if m.forward_called} + + assert depths == {"": 0, ".layers.0": 1, ".layers.1": 1, ".head": 1, ".layers.0.mlp": 2, ".layers.1.mlp": 2} + + +@pytest.mark.parametrize( + ("depth", "expected"), + [ + (0, [""]), + (1, [".layers.0", ".layers.1", ".head"]), + (2, [".layers.0.mlp", ".layers.1.mlp", ".head"]), + (5, [".layers.0.mlp", ".layers.1.mlp", ".head"]), + ], +) +def test_get_modules_keeps_shallower_leaves(depth, expected): + """`head` has no executed children, so it stays in every cut below its own depth.""" + modules = _executed_encoder_inspector(depth).get_modules() + + assert sorted(m.object_path for m in modules) == sorted(expected) + + +def test_get_modules_can_be_called_twice(): + inspector = _executed_encoder_inspector(depth=2) + first = inspector.get_modules() + + assert inspector.get_modules() == first + + +@pytest.mark.parametrize( + ("depth", "expected"), + [ + (0, [".encoder", ".decoder"]), + (1, [".encoder.mlp", ".decoder.mlp"]), + ], +) +def test_get_modules_from_non_module_root(depth, expected): + """Executed members of a plain object are at depth 0.""" + + class Pipeline: + def __init__(self): + self.encoder = _Block() + self.decoder = _Block() + + def __call__(self, x): + return self.decoder(self.encoder(x)) + + pipeline = Pipeline() + inspector = ModuleInspector(depth=depth) + inspector.inspect(pipeline) + pipeline(torch.randn(1, 8)) + + assert [m.object_path for m in inspector.get_modules()] == expected + + +def test_negative_depth(): + with pytest.raises(ValueError): + ModuleInspector(depth=-1) + + def test_wrap_forward_methods(simple_model): """Test wrapping forward methods.""" inspector = ModuleInspector() @@ -554,7 +677,7 @@ def test_get_modules_with_parakeet_rnnt_like_model(parakeet_rnnt_like_model): def test_get_modules_with_parakeet_rnnt_like_model_depth_1(parakeet_rnnt_like_model): """Test getting executed modules from ParakeetRNNTLikeModel.""" - inspector = ModuleInspector(min_depth=1) + inspector = ModuleInspector(depth=1) inspector.inspect(parakeet_rnnt_like_model) parakeet_rnnt_like_model(torch.randn(1, 10)) @@ -617,7 +740,7 @@ def run(self, x): def test_second_references_to_module_custom_class(): model = SecondReferencesToModuleCustomClass() - inspector = ModuleInspector(min_depth=1) + inspector = ModuleInspector(depth=1) inspector.inspect(model) model.run(torch.randn(1, 10)) @@ -649,7 +772,7 @@ def test_second_references_to_module_in_module(): assert "unet2" not in set(vars(model).keys()) assert "unet2_unused" not in set(vars(model).keys()) - inspector = ModuleInspector(min_depth=1) + inspector = ModuleInspector(depth=1) inspector.inspect(model) model(torch.randn(1, 10)) diff --git a/tests/unit/torch/inspecting/test_wrapping.py b/tests/unit/torch/inspecting/test_wrapping.py index 4ad798ab..d29d6968 100644 --- a/tests/unit/torch/inspecting/test_wrapping.py +++ b/tests/unit/torch/inspecting/test_wrapping.py @@ -451,7 +451,7 @@ def test_tune_reaches_every_module_with_a_repeated_name(tmp_path, monkeypatch): monkeypatch.setenv("TORCH_TWEAK_CACHE_DIR", str(tmp_path)) model = _EncoderDecoder().eval() sample = torch.randn(8) - modules_info = inspect(model, sample, number_of_iterations=1, warmup_iterations=1, min_depth=3) + modules_info = inspect(model, sample, number_of_iterations=1, warmup_iterations=1, depth=2) strategy = FirstWinsStrategy(backends=[TorchEagerBackend()]) wrap(model, modules_info.get_modules(), strategy=strategy) diff --git a/torch_tweak/torch/inspecting/inspecting.py b/torch_tweak/torch/inspecting/inspecting.py index 425d8326..56eb8171 100644 --- a/torch_tweak/torch/inspecting/inspecting.py +++ b/torch_tweak/torch/inspecting/inspecting.py @@ -31,26 +31,33 @@ def inspect( inference_function: Callable | None = None, number_of_iterations: int = DEFAULT_INSPECT_ITERATIONS, warmup_iterations: int = DEFAULT_WARMUP_ITERATIONS, - min_depth: int = 0, - max_depth: int = 5, + depth: int = 0, + warn_if_no_modules: bool = True, ) -> InspectedModulesInfo: """Inspect provided callable object searching for nn.Module members executed as part of forward pass. + The depth of a module is the number of executed modules above it. Depth 0 holds the outermost + executed modules: `obj` itself when it is an executed module, otherwise its executed members. + Args: obj: Callable object to inspect. dataset: List of tuples with batch size and input. inference_function: Custom inference function to use for inspection, obj is used by default. number_of_iterations: Number of iterations to run for inference. warmup_iterations: Number of iterations to run for warmup. - min_depth: Minimum depth of the modules to inspect, if root level modules is not working, try to increase this value - max_depth: Maximum depth of the modules to inspect + depth: Depth at which the model is cut into modules. Executed modules at this depth are returned, + together with shallower executed modules that have no executed children. + Increase it when the outermost modules cannot be tuned. + warn_if_no_modules: Warn when the walk stops at the path limit before any module is registered. + The path limit is calculated from `depth`. Increase `depth` to walk further. + Returns: InspectedModulesInfo object. """ setup_logging(format_string=LOG_FORMAT) logger.info("Inspecting object searching for executed nn.Module members.") - model_inspector = ModuleInspector(min_depth=min_depth, max_depth=max_depth) + model_inspector = ModuleInspector(depth=depth) model_inspector.inspect(obj) # If no inference function is provided, use the obj @@ -78,6 +85,18 @@ def inspect( modules = model_inspector.get_modules() logger.info("Inspection done. Found %d candidate modules for tuning.", len(modules)) + if ( + warn_if_no_modules + and not modules + and not model_inspector._module_info + and model_inspector._stopped_at_path_limit + ): + logger.warning( + "No modules found. The walk stopped at path depth %d, calculated from depth=%d. " + "Increase depth to walk further.", + model_inspector._max_path_depth, + depth, + ) inspected_modules_info = InspectedModulesInfo(total_execution_time, number_of_iterations) for module in modules: diff --git a/torch_tweak/torch/inspecting/module_info.py b/torch_tweak/torch/inspecting/module_info.py index 83e61958..ee706e03 100644 --- a/torch_tweak/torch/inspecting/module_info.py +++ b/torch_tweak/torch/inspecting/module_info.py @@ -24,7 +24,20 @@ class ModuleInfo: input_types: list[dict[str, Any]] = field(default_factory=list) # unused, even in tests output_types: list[dict[str, Any]] = field(default_factory=list) # used in tests only object_path: str | None = None - depth: int = 0 + + @property + def executed_parent(self) -> Optional["ModuleInfo"]: + """The closest ancestor in the object tree whose forward was called.""" + parent = self.parent + while parent is not None and not parent.forward_called: + parent = parent.parent + return parent + + @property + def depth(self) -> int: + """The number of executed ancestors in the object tree.""" + parent = self.executed_parent + return 0 if parent is None else parent.depth + 1 @property def module_type(self) -> type: diff --git a/torch_tweak/torch/inspecting/module_inspector.py b/torch_tweak/torch/inspecting/module_inspector.py index da975163..7266c499 100644 --- a/torch_tweak/torch/inspecting/module_inspector.py +++ b/torch_tweak/torch/inspecting/module_inspector.py @@ -20,6 +20,12 @@ dot notation, even for dictionaries and lists. (e.g., '.list.0.layers`, '.dict.key.layers`, `.encoder.layers.0.self_attn.q_proj.weight`). +The depth of a module is the number of executed modules above it in the object +tree, see `ModuleInfo.depth`. Containers such as lists, dicts, `nn.ModuleList`, +and modules whose forward never ran do not count. +The walk stops at a path depth calculated from `depth`. +Modules past that path are not registered. + Environment variables that could be used: TORCH_TWEAK_INSPECT_DEBUG: @@ -34,7 +40,7 @@ import time from collections.abc import Callable -from dataclasses import dataclass +from dataclasses import dataclass, replace from functools import cache, wraps from logging import getLogger from typing import Any @@ -52,7 +58,7 @@ logger = getLogger(__name__) -DEFAULT_MAX_RECURSION_DEPTH = 5 +EXTRA_PATH_SEGMENTS = 4 DEFAULT_INSPECT_DEBUG = False @@ -63,7 +69,7 @@ class InspectContext: """Context for inspecting modules.""" - depth: int = 0 + path_depth: int = 0 object_path: str = "" module_parent: ModuleInfo | None = None @@ -87,36 +93,41 @@ def create_module_info(self, module: torch.nn.Module | list | dict | Any) -> Mod module=module, parent=self.module_parent, object_path=self.object_path, - depth=self.depth, ) def next(self, name: str = "", parent: ModuleInfo | None = None) -> "InspectContext": """Get the next inspect context - increment depth and add name to object path.""" return InspectContext( - depth=self.depth + 1, + path_depth=self.path_depth + 1, object_path=f"{self.object_path}.{name}" if name else self.object_path, module_parent=parent, ) def clone(self, **kwargs) -> "InspectContext": """Get the inspect context, with optional changes.""" - return InspectContext( - depth=kwargs.get("depth", self.depth), - object_path=kwargs.get("object_path", self.object_path), - module_parent=kwargs.get("module_parent", self.module_parent), - ) + return replace(self, **kwargs) class ModuleInspector: """Class for inspecting PyTorch modules and tracking their execution.""" - def __init__(self, min_depth: int = 0, max_depth: int = 5): - """Initialize the module inspector.""" + def __init__(self, depth: int = 0): + """Initialize the module inspector. + + Args: + depth: Depth at which `get_modules` cuts the model. + """ + if depth < 0: + raise ValueError(f"Expected depth >= 0, got {depth}") self._module_info: dict[Any, ModuleInfo] = {} self._original_forward: dict[Any, Any] = {} self._inspected_objects: set = set() # Track objects that have been inspected - self._min_recursion_depth = min_depth - self._max_recursion_depth = max_depth + self._depth = depth + # Depth is known only after execution, so traversal is bounded by the object path. + # One depth level allows two path segments: the module and a container, as in `layers.0`. + # EXTRA_PATH_SEGMENTS covers further containers and modules that never run. + self._max_path_depth = 2 * (depth + 1) + EXTRA_PATH_SEGMENTS + self._stopped_at_path_limit = False self._execution_parent_module: list[ModuleInfo] = [] # used for logging the execution path only if _get_inspect_debug(): self._debug_inspecting_functions() @@ -140,41 +151,21 @@ def reset(self) -> None: self._module_info.clear() self._original_forward.clear() + self._stopped_at_path_limit = False - # fixme: this is wrong if depth is smaller than _min_recursion_depth - some branches are skipped, without even warning - # in case of min_depth=0, it will skip all modules except the root one (or in unusual cases all roots) def get_modules(self) -> list[ModuleInfo]: - """Get list of top-level executed modules or their first executed children. + """Get executed modules that cut the model at `depth`. + + The result holds every executed module at `depth`, plus every shallower executed module + without executed children. No returned module is nested in another one. Returns: - List of modules that were executed at the top level or their first executed children - if the parent wasn't executed. + Modules in inspection order. """ - executed_modules = [] - - # removing parents calls - for module_info in self._module_info.values(): - if module_info.depth < self._min_recursion_depth: - module_info.forward_called = False - - def parent_called(module_info: ModuleInfo) -> bool: - if module_info.parent is None: - return False - - if module_info.parent.forward_called: - return True - - return parent_called(module_info.parent) - - for module_info in self._module_info.values(): - if ( - module_info.depth >= self._min_recursion_depth - and module_info.forward_called - and not parent_called(module_info) - ): - executed_modules.append(module_info) - - return executed_modules + executed = [m for m in self._module_info.values() if m.forward_called] + candidates = [m for m in executed if m.depth <= self._depth] + parents = {id(m.executed_parent) for m in candidates if m.executed_parent is not None} + return [m for m in candidates if id(m) not in parents] def _inspect_object(self, obj: Any, *, context: InspectContext) -> None: """Inspect an object and its members for PyTorch modules. @@ -184,7 +175,8 @@ def _inspect_object(self, obj: Any, *, context: InspectContext) -> None: context: The context of the inspection """ # Check recursion depth - if context.depth > self._max_recursion_depth: + if context.path_depth > self._max_path_depth: + self._stopped_at_path_limit = True return # Skip Python built-in objects @@ -399,8 +391,8 @@ def _wrap_debug_inspect(self, method: Callable, collection: bool = False) -> Cal @wraps(method) def inspect_debug(*args, **kwargs): context = kwargs["context"] - indent = "│" * context.depth - logger.debug("%s%s(%d)%s for %s", indent, "├", context.depth, method.__name__, context.object_path) + indent = "│" * context.path_depth + logger.debug("%s%s(%d)%s for %s", indent, "├", context.path_depth, method.__name__, context.object_path) try: return method(*args, **kwargs) finally: