neurarch-trace 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 neurarch-ai
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,124 @@
1
+ Metadata-Version: 2.4
2
+ Name: neurarch-trace
3
+ Version: 0.1.0
4
+ Summary: Run one forward pass over a PyTorch model and write a .neurarch.json graph with real per-layer shapes, for neurarch-mcp.
5
+ Author: neurarch-ai
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/neurarch-ai/neurarch-mcp
8
+ Project-URL: Repository, https://github.com/neurarch-ai/neurarch-mcp
9
+ Keywords: pytorch,mcp,neurarch,model-graph,tracing
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
13
+ Requires-Python: >=3.9
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: torch>=2.0
17
+ Provides-Extra: hf
18
+ Requires-Dist: transformers; extra == "hf"
19
+ Provides-Extra: dev
20
+ Requires-Dist: pytest; extra == "dev"
21
+ Dynamic: license-file
22
+
23
+ # neurarch-trace
24
+
25
+ Run one forward pass over a PyTorch model and write a `.neurarch.json` graph with
26
+ the real input and output shape of every layer. Point
27
+ [neurarch-mcp](https://github.com/neurarch-ai/neurarch-mcp) at that file and every
28
+ tool works: parameter counts, FLOPs, shape contracts, `lint_model`, `check_design`.
29
+
30
+ neurarch-mcp can already read a `.py` file by parsing it statically. Static parsing
31
+ cannot see tensor shapes, and it cannot follow a model that is built at runtime:
32
+ `AutoModel.from_pretrained(...)`, a timm factory, an architecture spread across a
33
+ dozen files. `neurarch-trace` closes that gap by instantiating the model and
34
+ watching the tensors go through it.
35
+
36
+ ## Install
37
+
38
+ ```
39
+ pip install neurarch-trace # torch >= 2.0
40
+ pip install 'neurarch-trace[hf]' # adds transformers for hf: targets
41
+ ```
42
+
43
+ ## Usage
44
+
45
+ ```
46
+ neurarch-trace <target> --input 1,3,224,224 [--input 1,128:long ...] [-o out.neurarch.json]
47
+ python -m neurarch_trace <target> ... # same thing
48
+ ```
49
+
50
+ A file and a class or factory in it:
51
+
52
+ ```
53
+ neurarch-trace models/resnet.py:ResNet18 --input 1,3,224,224 -o resnet18.neurarch.json
54
+ ```
55
+
56
+ An importable module and a factory function (called with no arguments):
57
+
58
+ ```
59
+ neurarch-trace my_pkg.model:build_model --input 1,3,224,224
60
+ ```
61
+
62
+ A Hugging Face checkpoint (defaults to `--input 1,16 --dtype long`, token ids):
63
+
64
+ ```
65
+ neurarch-trace hf:prajjwal1/bert-tiny -o bert-tiny.neurarch.json
66
+ ```
67
+
68
+ Then hand the graph to your agent:
69
+
70
+ ```
71
+ npx -y neurarch-mcp ./resnet18.neurarch.json
72
+ ```
73
+
74
+ `<target>` may name an `nn.Module` instance, an `nn.Module` subclass (instantiated
75
+ with no arguments), or a callable that returns one. `--input` is repeated for
76
+ multi-input forwards; a `:dtype` suffix (`1,128:long`) overrides `--dtype` for that
77
+ input. Random tensors are used, `torch.randn` for float dtypes and
78
+ `torch.randint(0, 1000, ...)` for integer ones.
79
+
80
+ Other flags: `--name` sets the graph name (default: the attribute or repo name),
81
+ `-o -` writes to stdout, `--depth N` stops descending at module depth N and
82
+ records the modules there as single nodes, `--verbose` shows the traceback on a
83
+ failure (otherwise a failure is one line on stderr and exit code 1).
84
+
85
+ ## The shape convention
86
+
87
+ Shapes in the graph are written **without the batch dimension**: `[3, 224, 224]`
88
+ for an image, `[128, 768]` for a token sequence. The first dim of every `--input`
89
+ is the batch and is stripped from every recorded shape. Neurarch reads a leading
90
+ dimension as the channel axis, so a shape that still carries its batch of 1 would
91
+ be read as a one-channel tensor and every downstream number would be wrong.
92
+
93
+ The trace runs in `eval()` mode on CPU with autograd enabled.
94
+
95
+ ## What ends up in the graph
96
+
97
+ - One node per leaf module call, typed with the Neurarch vocabulary (`conv2d`,
98
+ `linear`, `layerNorm`, `multiHeadAttention`, ...) and carrying the same
99
+ parameter keys the static parser emits, so the MCP estimators read them. A
100
+ module called twice is two nodes. Modules the mapping table does not know
101
+ become `customModule` with `className` and `paramCount` in their params.
102
+ - `scope` on every node is the dotted path of its parent module
103
+ (`encoder.layer.0.attention`), which is what the MCP block tools group by.
104
+ - Edges follow actual data flow. A tensor a layer returns is matched by identity;
105
+ anything produced by functional code in between (`x + residual`, `torch.cat`,
106
+ reshapes, RoPE math) is traced back through autograd to the layers it came from.
107
+ A residual add becomes an `add` node with two inputs, a concatenation a
108
+ `concatenate` node.
109
+ - Exactly one `input` node per `--input` and one `output` node.
110
+
111
+ ## Limits
112
+
113
+ - Functional ops other than add / cat / stack do not get nodes of their own; a
114
+ `torch.flatten` or `F.softmax` between two modules is invisible, and the two
115
+ modules are wired directly.
116
+ - Attribution runs on the autograd graph, so a forward wrapped in
117
+ `torch.no_grad()` or an integer-only path with no learnable parameter behind it
118
+ falls back to matching by dtype against the model inputs.
119
+ - The batch is assumed to be dim 0. Sequence-first layouts
120
+ (`nn.MultiheadAttention` with `batch_first=False`) come out with the sequence
121
+ length stripped instead.
122
+ - The mapping table lives in `neurarch_trace/mapping.py` and mirrors
123
+ `codeParser.ts` in the Neurarch app. If a torch module is missing there, it is
124
+ recorded as `customModule` rather than guessed.
@@ -0,0 +1,102 @@
1
+ # neurarch-trace
2
+
3
+ Run one forward pass over a PyTorch model and write a `.neurarch.json` graph with
4
+ the real input and output shape of every layer. Point
5
+ [neurarch-mcp](https://github.com/neurarch-ai/neurarch-mcp) at that file and every
6
+ tool works: parameter counts, FLOPs, shape contracts, `lint_model`, `check_design`.
7
+
8
+ neurarch-mcp can already read a `.py` file by parsing it statically. Static parsing
9
+ cannot see tensor shapes, and it cannot follow a model that is built at runtime:
10
+ `AutoModel.from_pretrained(...)`, a timm factory, an architecture spread across a
11
+ dozen files. `neurarch-trace` closes that gap by instantiating the model and
12
+ watching the tensors go through it.
13
+
14
+ ## Install
15
+
16
+ ```
17
+ pip install neurarch-trace # torch >= 2.0
18
+ pip install 'neurarch-trace[hf]' # adds transformers for hf: targets
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```
24
+ neurarch-trace <target> --input 1,3,224,224 [--input 1,128:long ...] [-o out.neurarch.json]
25
+ python -m neurarch_trace <target> ... # same thing
26
+ ```
27
+
28
+ A file and a class or factory in it:
29
+
30
+ ```
31
+ neurarch-trace models/resnet.py:ResNet18 --input 1,3,224,224 -o resnet18.neurarch.json
32
+ ```
33
+
34
+ An importable module and a factory function (called with no arguments):
35
+
36
+ ```
37
+ neurarch-trace my_pkg.model:build_model --input 1,3,224,224
38
+ ```
39
+
40
+ A Hugging Face checkpoint (defaults to `--input 1,16 --dtype long`, token ids):
41
+
42
+ ```
43
+ neurarch-trace hf:prajjwal1/bert-tiny -o bert-tiny.neurarch.json
44
+ ```
45
+
46
+ Then hand the graph to your agent:
47
+
48
+ ```
49
+ npx -y neurarch-mcp ./resnet18.neurarch.json
50
+ ```
51
+
52
+ `<target>` may name an `nn.Module` instance, an `nn.Module` subclass (instantiated
53
+ with no arguments), or a callable that returns one. `--input` is repeated for
54
+ multi-input forwards; a `:dtype` suffix (`1,128:long`) overrides `--dtype` for that
55
+ input. Random tensors are used, `torch.randn` for float dtypes and
56
+ `torch.randint(0, 1000, ...)` for integer ones.
57
+
58
+ Other flags: `--name` sets the graph name (default: the attribute or repo name),
59
+ `-o -` writes to stdout, `--depth N` stops descending at module depth N and
60
+ records the modules there as single nodes, `--verbose` shows the traceback on a
61
+ failure (otherwise a failure is one line on stderr and exit code 1).
62
+
63
+ ## The shape convention
64
+
65
+ Shapes in the graph are written **without the batch dimension**: `[3, 224, 224]`
66
+ for an image, `[128, 768]` for a token sequence. The first dim of every `--input`
67
+ is the batch and is stripped from every recorded shape. Neurarch reads a leading
68
+ dimension as the channel axis, so a shape that still carries its batch of 1 would
69
+ be read as a one-channel tensor and every downstream number would be wrong.
70
+
71
+ The trace runs in `eval()` mode on CPU with autograd enabled.
72
+
73
+ ## What ends up in the graph
74
+
75
+ - One node per leaf module call, typed with the Neurarch vocabulary (`conv2d`,
76
+ `linear`, `layerNorm`, `multiHeadAttention`, ...) and carrying the same
77
+ parameter keys the static parser emits, so the MCP estimators read them. A
78
+ module called twice is two nodes. Modules the mapping table does not know
79
+ become `customModule` with `className` and `paramCount` in their params.
80
+ - `scope` on every node is the dotted path of its parent module
81
+ (`encoder.layer.0.attention`), which is what the MCP block tools group by.
82
+ - Edges follow actual data flow. A tensor a layer returns is matched by identity;
83
+ anything produced by functional code in between (`x + residual`, `torch.cat`,
84
+ reshapes, RoPE math) is traced back through autograd to the layers it came from.
85
+ A residual add becomes an `add` node with two inputs, a concatenation a
86
+ `concatenate` node.
87
+ - Exactly one `input` node per `--input` and one `output` node.
88
+
89
+ ## Limits
90
+
91
+ - Functional ops other than add / cat / stack do not get nodes of their own; a
92
+ `torch.flatten` or `F.softmax` between two modules is invisible, and the two
93
+ modules are wired directly.
94
+ - Attribution runs on the autograd graph, so a forward wrapped in
95
+ `torch.no_grad()` or an integer-only path with no learnable parameter behind it
96
+ falls back to matching by dtype against the model inputs.
97
+ - The batch is assumed to be dim 0. Sequence-first layouts
98
+ (`nn.MultiheadAttention` with `batch_first=False`) come out with the sequence
99
+ length stripped instead.
100
+ - The mapping table lives in `neurarch_trace/mapping.py` and mirrors
101
+ `codeParser.ts` in the Neurarch app. If a torch module is missing there, it is
102
+ recorded as `customModule` rather than guessed.
@@ -0,0 +1,19 @@
1
+ """neurarch-trace: one forward pass, one .neurarch.json with real shapes.
2
+
3
+ from neurarch_trace import trace_model
4
+ graph = trace_model(model, [torch.randn(1, 3, 224, 224)], name="resnet")
5
+ """
6
+ from typing import Any, Dict, Optional, Sequence
7
+
8
+ __version__ = "0.1.0"
9
+
10
+
11
+ def trace_model(model, inputs: Sequence[Any], name: str = "model", depth: Optional[int] = None,
12
+ description: str = "") -> Dict[str, Any]:
13
+ """Trace `model` on `inputs` (batch first) and return the graph as a dict."""
14
+ from .tracer import trace
15
+ from .writer import build_graph
16
+ return build_graph(trace(model, list(inputs), depth=depth), name, description)
17
+
18
+
19
+ __all__ = ["trace_model", "__version__"]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,155 @@
1
+ """`neurarch-trace <target> --input 1,3,224,224 [-o out.neurarch.json]`."""
2
+ import argparse
3
+ import importlib
4
+ import importlib.util
5
+ import os
6
+ import re
7
+ import sys
8
+ from typing import List, Optional, Sequence, Tuple
9
+
10
+ import torch
11
+ import torch.nn as nn
12
+
13
+ from . import __version__
14
+ from .tracer import trace
15
+ from .writer import build_graph, write_graph
16
+
17
+ DTYPES = {
18
+ "float32": torch.float32, "float": torch.float32, "float16": torch.float16, "half": torch.float16,
19
+ "bfloat16": torch.bfloat16, "float64": torch.float64, "long": torch.long, "int64": torch.long,
20
+ "int": torch.int32, "int32": torch.int32, "bool": torch.bool,
21
+ }
22
+
23
+
24
+ class TraceError(Exception):
25
+ """A failure the user can act on; printed as one line without a traceback."""
26
+
27
+
28
+ def load_target(target: str) -> Tuple[nn.Module, str]:
29
+ """Resolve `module:attr`, `file.py:attr` or `hf:<repo>` into a model and a default name."""
30
+ if target.startswith("hf:"):
31
+ repo = target[3:]
32
+ try:
33
+ from transformers import AutoModel
34
+ except ImportError:
35
+ raise TraceError("hf: targets need transformers: pip install 'neurarch-trace[hf]'")
36
+ return AutoModel.from_pretrained(repo), re.sub(r"[^A-Za-z0-9_.-]+", "-", repo.split("/")[-1])
37
+
38
+ if ":" not in target:
39
+ raise TraceError("target must be module.path:attr, path/to/file.py:attr or hf:<repo-id>, got %r" % target)
40
+ modpath, attr = target.rsplit(":", 1)
41
+ if modpath.endswith(".py") or os.sep in modpath:
42
+ path = os.path.abspath(modpath)
43
+ if not os.path.isfile(path):
44
+ raise TraceError("no such file: %s" % path)
45
+ sys.path.insert(0, os.path.dirname(path))
46
+ spec = importlib.util.spec_from_file_location(os.path.splitext(os.path.basename(path))[0], path)
47
+ mod = importlib.util.module_from_spec(spec)
48
+ spec.loader.exec_module(mod)
49
+ else:
50
+ # A bare module path is resolved from the working directory first, the
51
+ # way `python -m` would, so `models.resnet:ResNet18` works from a repo root.
52
+ sys.path.insert(0, os.getcwd())
53
+ mod = importlib.import_module(modpath)
54
+
55
+ obj = mod
56
+ for part in attr.split("."):
57
+ if not hasattr(obj, part):
58
+ raise TraceError("%s has no attribute %r" % (modpath, attr))
59
+ obj = getattr(obj, part)
60
+
61
+ if isinstance(obj, nn.Module):
62
+ model = obj
63
+ elif callable(obj):
64
+ try:
65
+ model = obj()
66
+ except Exception as e:
67
+ raise TraceError("%s() failed: %s: %s" % (attr, type(e).__name__, e))
68
+ if not isinstance(model, nn.Module):
69
+ raise TraceError("%s() returned %s, not an nn.Module" % (attr, type(model).__name__))
70
+ else:
71
+ raise TraceError("%s is %s; expected an nn.Module, an nn.Module subclass or a factory" % (attr, type(obj).__name__))
72
+ return model, attr.split(".")[-1]
73
+
74
+
75
+ def build_inputs(specs: Sequence[str], default_dtype: str) -> List[torch.Tensor]:
76
+ """`1,3,224,224` or `1,128:long`; the first dim is the batch."""
77
+ torch.manual_seed(0)
78
+ out = []
79
+ for spec in specs:
80
+ dims_text, _, dtype_name = spec.partition(":")
81
+ dtype_name = dtype_name or default_dtype
82
+ if dtype_name not in DTYPES:
83
+ raise TraceError("unknown dtype %r (one of %s)" % (dtype_name, ", ".join(sorted(DTYPES))))
84
+ try:
85
+ dims = [int(d) for d in dims_text.split(",") if d.strip()]
86
+ except ValueError:
87
+ raise TraceError("bad --input %r: expected comma-separated integers like 1,3,224,224" % spec)
88
+ if not dims:
89
+ raise TraceError("bad --input %r: no dims" % spec)
90
+ dtype = DTYPES[dtype_name]
91
+ if dtype == torch.bool:
92
+ out.append(torch.rand(dims) > 0.5)
93
+ elif dtype.is_floating_point:
94
+ out.append(torch.randn(dims, dtype=dtype))
95
+ else:
96
+ out.append(torch.randint(0, 1000, dims, dtype=dtype))
97
+ return out
98
+
99
+
100
+ def run(argv: Optional[Sequence[str]] = None) -> int:
101
+ p = argparse.ArgumentParser(
102
+ prog="neurarch-trace",
103
+ description="Run one forward pass over a PyTorch model and write a .neurarch.json graph with real shapes.",
104
+ )
105
+ p.add_argument("target", help="module.path:attr, path/to/file.py:attr, or hf:<repo-id>")
106
+ p.add_argument("--input", action="append", default=[], metavar="DIMS",
107
+ help="input dims, batch first, e.g. 1,3,224,224 or 1,128:long; repeat for multi-input forwards")
108
+ p.add_argument("--dtype", default=None, help="dtype for inputs without a :dtype suffix (default float32; long for hf:)")
109
+ p.add_argument("--name", default=None, help="graph name (default: the attr or repo name)")
110
+ p.add_argument("-o", "--output", default=None, help="output path (default ./<name>.neurarch.json; - for stdout)")
111
+ p.add_argument("--depth", type=int, default=None, help="stop descending at this module depth (default: leaf modules)")
112
+ p.add_argument("--verbose", action="store_true", help="show the full traceback on failure")
113
+ p.add_argument("--version", action="version", version="neurarch-trace " + __version__)
114
+ args = p.parse_args(argv)
115
+
116
+ is_hf = args.target.startswith("hf:")
117
+ specs = args.input or (["1,16"] if is_hf else [])
118
+ if not specs:
119
+ raise TraceError("--input is required (e.g. --input 1,3,224,224); it defaults only for hf: targets")
120
+ dtype = args.dtype or ("long" if is_hf else "float32")
121
+
122
+ model, default_name = load_target(args.target)
123
+ inputs = build_inputs(specs, dtype)
124
+ try:
125
+ nodes = trace(model, inputs, depth=args.depth)
126
+ except TraceError:
127
+ raise
128
+ except Exception as e:
129
+ raise TraceError("forward pass failed: %s: %s" % (type(e).__name__, e))
130
+
131
+ name = args.name or default_name
132
+ description = "Traced by neurarch-trace %s from %s with input %s" % (
133
+ __version__, args.target, " ".join(specs))
134
+ graph = build_graph(nodes, name, description)
135
+ out = args.output or ("./%s.neurarch.json" % name)
136
+ write_graph(graph, out)
137
+ if out != "-":
138
+ layers = sum(1 for c in graph["components"] if c["type"] not in ("input", "output"))
139
+ print("wrote %s (%d layers, %d connections)" % (out, layers, len(graph["connections"])))
140
+ return 0
141
+
142
+
143
+ def main(argv: Optional[Sequence[str]] = None) -> int:
144
+ verbose = "--verbose" in (argv if argv is not None else sys.argv[1:])
145
+ try:
146
+ return run(argv)
147
+ except Exception as e:
148
+ if verbose:
149
+ raise
150
+ print("neurarch-trace: %s" % e, file=sys.stderr)
151
+ return 1
152
+
153
+
154
+ if __name__ == "__main__":
155
+ sys.exit(main())
@@ -0,0 +1,203 @@
1
+ """torch module class -> Neurarch component type and param keys.
2
+
3
+ Source of truth: `src/utils/codeParser.ts` in the Neurarch app (the static
4
+ parser that neurarch-mcp vendors). The MCP param and FLOPs estimators key off
5
+ the exact param names that parser emits, so every entry here mirrors what
6
+ `parseLayerDefinition` would produce for the same `nn.X(...)` call. Adding a
7
+ type that codeParser does not emit is fine as long as it exists in
8
+ `src/lib/types.ts`; renaming a param key is not.
9
+ """
10
+ from typing import Any, Callable, Dict, Optional, Tuple
11
+
12
+ import torch.nn as nn
13
+
14
+ Params = Dict[str, Any]
15
+
16
+
17
+ def _first(v: Any) -> Any:
18
+ # torch stores kernel_size / stride / padding as tuples; the graph stores the
19
+ # scalar codeParser reads off the source when all dims agree.
20
+ if isinstance(v, (tuple, list)):
21
+ return v[0] if all(x == v[0] for x in v) else list(v)
22
+ return v
23
+
24
+
25
+ def _conv(m: nn.Module) -> Params:
26
+ p: Params = {
27
+ "inChannels": m.in_channels,
28
+ "outChannels": m.out_channels,
29
+ "kernelSize": _first(m.kernel_size),
30
+ "stride": _first(m.stride),
31
+ "padding": _first(m.padding),
32
+ }
33
+ if _first(m.dilation) != 1:
34
+ p["dilation"] = _first(m.dilation)
35
+ if m.groups != 1:
36
+ p["groups"] = m.groups
37
+ if m.bias is None:
38
+ p["bias"] = False
39
+ return p
40
+
41
+
42
+ def _conv2d(m: nn.Module) -> Tuple[str, Params]:
43
+ # codeParser's ViT heuristic: a wide conv whose stride equals its kernel on
44
+ # a few input channels is a patch projection, not a feature extractor.
45
+ k, s = _first(m.kernel_size), _first(m.stride)
46
+ if isinstance(k, int) and k > 4 and k == s and m.in_channels <= 4:
47
+ return "patchEmbed", {"patchSize": k, "embedDim": m.out_channels, "inChans": m.in_channels}
48
+ return "conv2d", _conv(m)
49
+
50
+
51
+ def _conv_t2d(m: nn.Module) -> Params:
52
+ p = _conv(m)
53
+ if _first(m.output_padding) != 0:
54
+ p["outputPadding"] = _first(m.output_padding)
55
+ return p
56
+
57
+
58
+ def _pool(m: nn.Module) -> Params:
59
+ return {"kernelSize": _first(m.kernel_size), "stride": _first(m.stride or m.kernel_size)}
60
+
61
+
62
+ def _adaptive_avg(m: nn.Module) -> Tuple[str, Params]:
63
+ out = m.output_size if isinstance(m.output_size, (tuple, list)) else (m.output_size,)
64
+ if all(o in (1, None) for o in out):
65
+ return "globalAvgPool2d", {}
66
+ return "adaptiveAvgPool2d", {"outputSize": _first(m.output_size)}
67
+
68
+
69
+ def _recurrent(m: nn.Module) -> Params:
70
+ p: Params = {"inputSize": m.input_size, "hiddenSize": m.hidden_size, "numLayers": m.num_layers}
71
+ if m.bidirectional:
72
+ p["bidirectional"] = True
73
+ return p
74
+
75
+
76
+ def _norm_shape(m: nn.Module) -> Params:
77
+ return {"normalizedShape": _first(tuple(m.normalized_shape))}
78
+
79
+
80
+ # class name -> (component type, param extractor). An extractor may instead
81
+ # return (type, params) itself when the type depends on the module's config.
82
+ TORCH_CLASS_MAP: Dict[str, Tuple[Optional[str], Callable[[nn.Module], Any]]] = {
83
+ "Linear": ("linear", lambda m: dict(
84
+ {"inFeatures": m.in_features, "outFeatures": m.out_features}, **({} if m.bias is not None else {"bias": False}))),
85
+ "Conv2d": (None, _conv2d),
86
+ "ConvTranspose2d": ("transposeConv2d", _conv_t2d),
87
+ "Conv1d": ("conv1d", _conv),
88
+ "Conv3d": ("conv3d", _conv),
89
+ "MaxPool2d": ("maxpool2d", _pool),
90
+ "AvgPool2d": ("avgpool2d", _pool),
91
+ "MaxPool1d": ("maxpool1d", _pool),
92
+ "AvgPool1d": ("avgpool1d", _pool),
93
+ "AdaptiveAvgPool2d": (None, _adaptive_avg),
94
+ "AdaptiveMaxPool2d": ("adaptiveMaxPool2d", lambda m: {"outputSize": _first(m.output_size)}),
95
+ "Upsample": ("upsample", lambda m: {"scaleFactor": _first(m.scale_factor)}),
96
+ "Dropout": ("dropout", lambda m: {"p": m.p}),
97
+ "Dropout2d": ("dropout", lambda m: {"p": m.p}),
98
+ "BatchNorm1d": ("batchNorm", lambda m: {"numFeatures": m.num_features}),
99
+ "BatchNorm2d": ("batchNorm", lambda m: {"numFeatures": m.num_features}),
100
+ "BatchNorm3d": ("batchNorm", lambda m: {"numFeatures": m.num_features}),
101
+ "LayerNorm": ("layerNorm", _norm_shape),
102
+ "RMSNorm": ("rmsNorm", _norm_shape),
103
+ "GroupNorm": ("groupNorm", lambda m: {"numGroups": m.num_groups, "numChannels": m.num_channels}),
104
+ "InstanceNorm2d": ("instanceNorm", lambda m: {"numFeatures": m.num_features}),
105
+ "Embedding": ("embedding", lambda m: {"vocabSize": m.num_embeddings, "embeddingDim": m.embedding_dim}),
106
+ "EmbeddingBag": ("embeddingBag", lambda m: {"vocabSize": m.num_embeddings, "embeddingDim": m.embedding_dim}),
107
+ "MultiheadAttention": ("multiHeadAttention", lambda m: {"hiddenDim": m.embed_dim, "numHeads": m.num_heads}),
108
+ "TransformerEncoderLayer": ("transformerBlock", lambda m: {
109
+ "embedDim": m.self_attn.embed_dim, "numHeads": m.self_attn.num_heads, "ffDim": m.linear1.out_features}),
110
+ "LSTM": ("lstm", _recurrent),
111
+ "GRU": ("gru", _recurrent),
112
+ "RNN": ("rnn", _recurrent),
113
+ "Flatten": ("flatten", lambda m: {}),
114
+ "ReLU": ("relu", lambda m: {}),
115
+ "ReLU6": ("relu6", lambda m: {}),
116
+ "LeakyReLU": ("leakyRelu", lambda m: {"negativeSlope": m.negative_slope}),
117
+ "ELU": ("elu", lambda m: {}),
118
+ "PReLU": ("prelu", lambda m: {"numParameters": m.num_parameters}),
119
+ "SELU": ("selu", lambda m: {}),
120
+ "GELU": ("gelu", lambda m: {}),
121
+ "SiLU": ("swish", lambda m: {}),
122
+ "Mish": ("mish", lambda m: {}),
123
+ "Hardswish": ("hardSwish", lambda m: {}),
124
+ "Hardsigmoid": ("hardSigmoid", lambda m: {}),
125
+ "Sigmoid": ("sigmoid", lambda m: {}),
126
+ "Tanh": ("tanh", lambda m: {}),
127
+ "Softmax": ("softmax", lambda m: {}),
128
+ "LogSoftmax": ("logSoftmax", lambda m: {}),
129
+ "Softplus": ("softplus", lambda m: {}),
130
+ "GLU": ("glu", lambda m: {}),
131
+ }
132
+
133
+ # Modules that pass their input through untouched. They get no node, so the
134
+ # tensor keeps pointing at its real producer and no fake hop appears.
135
+ PASSTHROUGH_CLASSES = {"Identity"}
136
+
137
+ # Copied from codeParser.ts CUSTOM_CLASS_MAP (lowercased class name -> type).
138
+ # Only consulted when --depth stops descent at a container the torch table
139
+ # does not know, so a `LlamaAttention` cut off at depth 3 is still an
140
+ # attention node rather than an anonymous custom box.
141
+ CUSTOM_CLASS_MAP: Dict[str, str] = {
142
+ "positionalencoding": "positionalEncoding", "posencoding": "positionalEncoding",
143
+ "posemb": "positionalEncoding", "positionalembedding": "positionalEncoding",
144
+ "rotaryembedding": "rope", "ropeembedding": "rope",
145
+ "multiheadattention": "multiHeadAttention", "multiheadattn": "multiHeadAttention",
146
+ "causalselfattention": "causalAttention", "selfattn": "selfAttention",
147
+ "selfattention": "selfAttention", "attention": "attention", "crossattention": "crossModalAttention",
148
+ "llamaattention": "groupedQueryAttention", "mistralattention": "groupedQueryAttention",
149
+ "mixtralattention": "groupedQueryAttention", "qwenattention": "groupedQueryAttention",
150
+ "gemmaattention": "groupedQueryAttention", "phi3attention": "groupedQueryAttention",
151
+ "falcon7battention": "groupedQueryAttention", "groupedqueryattention": "groupedQueryAttention",
152
+ "gqaattention": "groupedQueryAttention",
153
+ "feedforward": "feedForward", "feedforwardnetwork": "feedForward", "ffn": "feedForward", "mlp": "feedForward",
154
+ "llamamlp": "swiglu", "mistralmpl": "swiglu", "mixtralmlp": "swiglu", "qwenmlp": "swiglu",
155
+ "gemmamlp": "swiglu", "phi3mlp": "swiglu", "swiglu": "swiglu", "gatedmlp": "swiglu",
156
+ "mixtralsparsemoeblock": "moeLayer", "moelayer": "moeLayer", "expertlayer": "moeLayer", "sparsemlp": "moeLayer",
157
+ "block": "transformerBlock", "gptblock": "transformerBlock", "bertlayer": "transformerBlock",
158
+ "bertblock": "transformerBlock", "encoderlayer": "transformerBlock", "decoderlayer": "transformerBlock",
159
+ "transformerblock": "transformerBlock", "transformerlayer": "transformerBlock",
160
+ "transformer": "transformerBlock", "visionblock": "transformerBlock",
161
+ "llamadecoderlayer": "transformerBlock", "llamadecoderblock": "transformerBlock",
162
+ "mistraldecoderlayer": "transformerBlock", "mixtraldecoderlayer": "transformerBlock",
163
+ "qwendecoderlayer": "transformerBlock", "gemmadecoderlayer": "transformerBlock",
164
+ "phi3decoderlayer": "transformerBlock",
165
+ "visiontransformer": "transformerBlock", "vitblock": "transformerBlock", "vitlayer": "transformerBlock",
166
+ "patchembed": "patchEmbed", "patchembedding": "patchEmbed", "patchprojection": "patchEmbed",
167
+ "seblock": "seBlock", "squeezeexcitation": "seBlock", "channelattention": "seBlock",
168
+ "resblock": "residual", "residualblock": "residual", "resnetblock": "residual", "bottleneck": "residual",
169
+ }
170
+
171
+ # Attribute names real-world blocks use for the two numbers the estimators
172
+ # need most. Read off a container only when it is cut off by --depth.
173
+ _DIM_ATTRS = ("embed_dim", "d_model", "hidden_size", "dim", "n_embd")
174
+ _HEAD_ATTRS = ("num_heads", "n_head", "nhead", "num_attention_heads")
175
+
176
+
177
+ def _sniff(m: nn.Module, names: Tuple[str, ...]) -> Optional[int]:
178
+ for n in names:
179
+ v = getattr(m, n, None)
180
+ if isinstance(v, int) and not isinstance(v, bool):
181
+ return v
182
+ return None
183
+
184
+
185
+ def map_module(m: nn.Module) -> Tuple[str, Params]:
186
+ """Return (component type, params) for one module treated as a graph node."""
187
+ cls = type(m).__name__
188
+ entry = TORCH_CLASS_MAP.get(cls)
189
+ if entry is not None:
190
+ typ, extract = entry
191
+ result = extract(m)
192
+ return (typ, result) if typ is not None else result
193
+
194
+ n_params = sum(p.numel() for p in m.parameters())
195
+ params: Params = {"className": cls, "paramCount": n_params}
196
+ typ = CUSTOM_CLASS_MAP.get(cls.lower(), "customModule")
197
+ if typ != "customModule":
198
+ d, h = _sniff(m, _DIM_ATTRS), _sniff(m, _HEAD_ATTRS)
199
+ if d is not None:
200
+ params["embedDim"] = d
201
+ if h is not None:
202
+ params["numHeads"] = h
203
+ return typ, params