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.
- neurarch_trace-0.1.0/LICENSE +21 -0
- neurarch_trace-0.1.0/PKG-INFO +124 -0
- neurarch_trace-0.1.0/README.md +102 -0
- neurarch_trace-0.1.0/neurarch_trace/__init__.py +19 -0
- neurarch_trace-0.1.0/neurarch_trace/__main__.py +5 -0
- neurarch_trace-0.1.0/neurarch_trace/cli.py +155 -0
- neurarch_trace-0.1.0/neurarch_trace/mapping.py +203 -0
- neurarch_trace-0.1.0/neurarch_trace/tracer.py +252 -0
- neurarch_trace-0.1.0/neurarch_trace/writer.py +66 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/PKG-INFO +124 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/SOURCES.txt +16 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/dependency_links.txt +1 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/entry_points.txt +2 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/requires.txt +7 -0
- neurarch_trace-0.1.0/neurarch_trace.egg-info/top_level.txt +1 -0
- neurarch_trace-0.1.0/pyproject.toml +33 -0
- neurarch_trace-0.1.0/setup.cfg +4 -0
- neurarch_trace-0.1.0/tests/test_tracer.py +228 -0
|
@@ -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,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
|