Skip to content

Commit 6008c27

Browse files
committed
Update model inspector documentation
- Minor updates to eager mode model inspector
1 parent a87b440 commit 6008c27

7 files changed

Lines changed: 244 additions & 179 deletions

File tree

docs/src/palettization/config.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ op_config = OpKMeansPalettizerConfig(
116116

117117
## Examples
118118

119-
Several examples below configure specific module types or module names. To determine these for your model, see [How to get names + types](../quantization/config.md#how-to-get-names--types-for-modules-and-ops). Since palettization only supports eager execution mode, only the eager mode guidance in that section is relevant.
119+
Several examples below configure specific module types or module names. To determine these for your model, use {class}`~coreai_opt.inspection.ModelInspector` with `execution_mode="eager"` — see [Inspecting Model Structure](../utils/model_inspection.md). Palettization supports eager mode only.
120120

121121
### Apply 4-bit palettization globally, 8-bit to linear layers
122122

docs/src/quantization/config.md

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ The defaults are:
184184

185185
In [Quantization Overview](overview.md) we saw how to use the default `W_INT8_A_INT8` config. [Config classes and their defaults](#config-classes-and-their-defaults) described the default settings in `QuantizerConfig()`, `ModuleQuantizerConfig()`, and `OpQuantizerConfig()`. Let us now see how to configure quantization when non-default settings are desired.
186186

187-
Several examples below configure specific module names, module types, op names, or op types. To determine these for your model, see [How to get names + types for modules and ops](#how-to-get-names-types-for-modules-and-ops) (eager mode) or [Inspecting Model Structure](../utils/model_inspection.md) (graph mode).
187+
Several examples below configure specific module names, module types, op names, or op types. To determine these for your model, see [Inspecting Model Structure](../utils/model_inspection.md).
188188

189189
### Example: `W_MXFP4_A_FP8` applied to all supported ops
190190

@@ -857,7 +857,7 @@ classDiagram
857857

858858
## How to get names + types for modules and ops
859859

860-
**Graph mode** (for `module_name_configs`, `module_type_configs`, `op_name_config`, `op_type_config`): use {class}`~coreai_opt.inspection.ModelInspector` to discover module names, module types, op names, and op types.
860+
Use {class}`~coreai_opt.inspection.ModelInspector` to discover module names, module types, op names, and op types for both graph and eager execution modes.
861861

862862
```python
863863
import torch
@@ -866,40 +866,12 @@ from coreai_opt.inspection import ModelInspector
866866
867867
model = nn.Sequential(nn.Linear(10, 20), nn.ReLU(), nn.Linear(20, 5))
868868
inspector = ModelInspector(
869-
model, example_inputs=(torch.randn(1, 10),), execution_mode="graph"
869+
# Use execution_mode="eager" for eager mode inspection.
870+
model,
871+
example_inputs=(torch.randn(1, 10),),
872+
execution_mode="graph",
870873
)
871874
print(inspector.format_summary())
872875
```
873876

874-
See [Inspecting Model Structure](../utils/model_inspection.md) for full usage and examples.
875-
876-
**Eager mode**: module names (for `module_name_configs`) can be obtained by inspecting `model.named_modules()`. This includes all modules in the model (nested and leaf). The names align with the structure of modules defined in code.
877-
878-
Op names can be constructed by referring to the parent module and the op in it. Example:
879-
880-
```python
881-
class TwoAddModule(torch.nn.Module):
882-
def forward(self, x):
883-
a = x + x
884-
b = a + a
885-
return b
886-
887-
888-
class Model(torch.nn.Module):
889-
def __init__(self):
890-
super().__init__()
891-
self.submodule_a = TwoAddModule()
892-
self.submodule_b = TwoAddModule()
893-
894-
def forward(self, x):
895-
x = self.submodule_a(x)
896-
x = self.submodule_b(x)
897-
return x
898-
899-
900-
# the names of ops in the model will be :
901-
# - submodule_a.add
902-
# - submodule_a.add_1
903-
# - submodule_b.add
904-
# - submodule_b.add_1
905-
```
877+
See [Inspecting Model Structure](../utils/model_inspection.md) for full usage, examples, and a comparison of graph and eager mode op naming.

docs/src/utils/model_inspection.md

Lines changed: 141 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
`coreai-opt` configs reference module names, module types, op names, and op types to target specific parts of a model. Before writing a config, you need to know exactly which strings your model exposes. {class}`~coreai_opt.inspection.ModelInspector` discovers these automatically and provides query methods corresponding to each config key type (`op_type_config`, `op_name_config`, `module_name_configs`, `module_type_configs`).
44

5-
:::{note}
6-
`ModelInspector` currently supports **graph execution mode only**. Eager mode support is planned. For eager mode op naming, see [How to get names + types](../quantization/config.md#how-to-get-names--types-for-modules-and-ops).
7-
:::
5+
## Execution Modes
6+
7+
`ModelInspector` supports two execution modes, selected via the `execution_mode` argument:
8+
9+
- **Graph mode** (`execution_mode="graph"`): Exports the model with `torch.export` and walks the resulting FX graph. Op names are global identifiers assigned during export (for example, `"linear"`, `"linear_1"`). The compressor must be `Quantizer` or `None`.
10+
- **Eager mode** (`execution_mode="eager"`): Intercepts operations during a live forward pass. Op names are module-qualified identifiers that reflect the module hierarchy (for example, `"linear1.linear"`, `"linear2.linear"`). This mode supports both `Quantizer` and `KMeansPalettizer` as the compressor.
11+
12+
Choose graph mode when you need exact parity with the exported graph, and eager mode when you want op names that directly match the module hierarchy or need to inspect models not yet exportable.
813

914
## Basic Usage
1015

@@ -38,97 +43,123 @@ inspector = ModelInspector(
3843
compressor=Quantizer,
3944
)
4045

41-
# Print a module-hierarchy tree showing ops, source locations, and connectivity
46+
# Print a module-hierarchy tree showing ops, connectivity, and source locations
4247
print(inspector.format_summary())
4348
```
4449

45-
Note the use of `compressor=Quantizer` in the list of arguments to `ModelInspector`. This filters the list of ops captured and displayed by `ModelInspector` to only those operations which are registered for compressibility by `Quantizer`. Omitting this argument allows for all ops to be captured and displayed.
50+
Pass `colorize=False` to suppress ANSI color codes, for example when writing to a file.
51+
52+
Note the use of `compressor=Quantizer`. This filters the captured and displayed ops to those registered as compressible by `Quantizer`. Omit this argument to capture and display all ops.
4653

4754
The above code produces output like the following (colors omitted for brevity):
4855

4956
```text
50-
Legend: ■ module_name (module_type) ◆ op_name [op_type]
51-
52-
(MyModel)
53-
module inputs: linear
54-
module outputs: linear_1
57+
Legend:
58+
■ module_name (module_type) ◆ op_name [op_type]
59+
60+
op inputs: {I: producer[N]} — I = op_input_spec index; N = output slot of the producing op
61+
op states: param_name — model parameter or buffer
62+
op outputs: {N: [consumers]} — N = output slot index; consumers = ops receiving that output
63+
untracked_N — input tensor whose producer was not intercepted (e.g. raw attribute or global tensor); still quantizable via op_input_spec
64+
module inputs: {I: [op[N], ...]} — I = module_input_spec index; op[N] = op and its input slot receiving data from outside; absent keys = non-quantizable
65+
module outputs: {I: op[N]} — I = module_output_spec index; op[N] = op and its output slot leaving the module; absent keys = non-quantizable
66+
67+
(__main__.MyModel)
68+
module inputs: {0: [linear[0]]}
69+
module outputs: {0: linear_1[0]}
5570
├── ■ linear1 (torch.nn.modules.linear.Linear)
56-
│ module inputs: linear
57-
│ module outputs: linear
71+
│ module inputs: {0: [linear[0]]}
72+
│ module outputs: {0: linear[0]}
5873
│ └── ◆ linear [linear]
59-
│ op inputs: x, linear1_weight, linear1_bias
60-
│ op outputs: relu
61-
│ filepath: my_model.py:16
62-
│ code: x = self.linear1(x)
74+
│ op inputs: {0: x[0]}
75+
│ op states: weight, bias
76+
│ op outputs: {0: [relu]}
6377
├── ■ relu (torch.nn.modules.activation.ReLU)
64-
│ module inputs: relu
65-
│ module outputs: relu
78+
│ module inputs: {0: [relu[0]]}
79+
│ module outputs: {0: relu[0]}
6680
└── ■ linear2 (torch.nn.modules.linear.Linear)
67-
module inputs: linear_1
68-
module outputs: linear_1
81+
module inputs: {0: [linear_1[0]]}
82+
module outputs: {0: linear_1[0]}
6983
└── ◆ linear_1 [linear]
70-
op inputs: relu, linear2_weight, linear2_bias
71-
op outputs: output
72-
filepath: my_model.py:18
73-
code: x = self.linear2(x)
84+
op inputs: {0: relu[0]}
85+
op states: weight, bias
86+
op outputs: {0: [output]}
7487
```
7588

76-
The output shows the model's module hierarchy and the ops within each module. Note in particular that since `relu` is not a registered compressible op by `Quantizer`, it does not show up as an operation within the `ReLU` module.
89+
Note that `relu` does not appear as an operation (``) within the `relu` module, because `ReLU` is not a compressible op in `Quantizer`. It still appears as a module node (``) and in connectivity lines such as `op outputs: {0: [relu]}` and `op inputs: {0: relu[0]}`, because the relu tensor passes through and connects the two linear ops.
7790

78-
Reading the tree:
91+
## Reading the Tree
7992

80-
- **Module name** and **module type** appear on module lines: `■ module_name (module_type)`. For example, `■ linear1 (torch.nn.modules.linear.Linear)``"linear1"` is the module name (usable in `module_name_configs`) and `"torch.nn.modules.linear.Linear"` is the module type (usable in `module_type_configs`).
81-
- **Op name** and **op type** appear on operation lines: `◆ op_name [op_type]`. For example, `◆ linear_1 [linear]``"linear_1"` is the op name (usable in `op_name_config`) and `"linear"` is the op type (usable in `op_type_config`).
82-
- **Op inputs/outputs** show connectivity between operations.
83-
- **filepath** and **code** (shown for user-defined modules) show where in your source code the operation originates.
93+
### Module lines
8494

85-
Using these strings directly in a config:
95+
Module lines use the form `■ module_name (module_type)`. For example, `■ linear1 (torch.nn.modules.linear.Linear)`:
8696

87-
```python
88-
config = QuantizerConfig(
89-
# Target a specific module by name
90-
module_name_configs={
91-
"linear1": ModuleQuantizerConfig(...),
92-
},
93-
# Target all modules of a given type
94-
module_type_configs={
95-
"torch.nn.modules.linear.Linear": ModuleQuantizerConfig(...),
96-
},
97-
)
97+
- `"linear1"` is the module name, usable in `module_name_configs`.
98+
- `"torch.nn.modules.linear.Linear"` is the module type, usable in `module_type_configs`.
9899

99-
# Op-level targeting within a ModuleQuantizerConfig
100-
config = QuantizerConfig(
101-
global_config=ModuleQuantizerConfig(
102-
# Target a specific op by name
103-
op_name_config={
104-
"linear_1": OpQuantizerConfig(...),
105-
},
106-
# Target all ops of a given type
107-
op_type_config={
108-
"linear": OpQuantizerConfig(...),
109-
},
110-
),
111-
)
112-
```
100+
**Module boundaries** appear indented under the module header:
101+
102+
- `module inputs: {I: [op[N], ...]}` — The activations entering this module from outside. `I` is the position in the module's input spec (matching `module_input_spec` in a config), `op` is the name of the first compressible op inside the module that receives data at that position, and `N` is the input slot on that op. A single external input can fan out to multiple ops. Keys absent from this dict correspond to non-quantizable positions (for example, state tensors or unused arguments).
103+
- `module outputs: {I: op[N]}` — The activations leaving this module. `I` is the position in the module's output spec, `op` is the compressible op producing that output, and `N` is the op's output slot. Absent keys correspond to non-quantizable positions.
104+
105+
### Op lines
106+
107+
Op lines use the form `◆ op_name [op_type]`. For example, `◆ linear_1 [linear]`:
108+
109+
- `"linear_1"` is the op name, usable in `op_name_config`.
110+
- `"linear"` is the op type, usable in `op_type_config`.
113111

114-
Pass `colorize=False` to suppress ANSI color codes (e.g., when writing to a file).
112+
**Op connectivity** appears indented under the op header:
113+
114+
- `op inputs: {I: producer[N]}` — Activation inputs only (parameters and buffers are on a separate line). `I` is the argument position (matching `op_input_spec` in a config), `producer` is the name of the op that produced this tensor, and `N` is the output slot of that producer. For example, `{0: relu[0]}` means argument 0 comes from output slot 0 of the `relu` op.
115+
- `op states: param_name, ...` — Model parameters and buffers consumed by this op. This line is omitted if the op takes no states.
116+
- `op outputs: {N: [consumer1, consumer2, ...]}``N` is the output slot index, and the list contains the names of all ops consuming that output.
117+
- `untracked_N` — Appears in place of a producer name when the input tensor's origin was not intercepted (for example, a raw module attribute or global tensor). These tensors are still quantizable via `op_input_spec`.
118+
- `filepath` and `code` — Source file and line of the call that produced the op, shown as dim text.
119+
120+
## Eager Mode
121+
122+
To inspect using eager mode, pass `execution_mode="eager"`. The same `MyModel` example above yields:
123+
124+
```text
125+
(__main__.MyModel)
126+
module inputs: {0: [linear1.linear[0]]}
127+
module outputs: {0: linear2.linear[0]}
128+
├── ■ linear1 (torch.nn.modules.linear.Linear)
129+
│ module inputs: {0: [linear1.linear[0]]}
130+
│ module outputs: {0: linear1.linear[0]}
131+
│ └── ◆ linear1.linear [linear]
132+
│ op inputs: {0: input_0}
133+
│ op states: weight, bias
134+
│ op outputs: {0: [relu.relu]}
135+
│ filepath: my_model.py:16
136+
├── ■ relu (torch.nn.modules.activation.ReLU)
137+
│ module inputs: {0: [relu.relu[0]]}
138+
│ module outputs: {0: relu.relu[0]}
139+
└── ■ linear2 (torch.nn.modules.linear.Linear)
140+
module inputs: {0: [linear2.linear[0]]}
141+
module outputs: {0: linear2.linear[0]}
142+
└── ◆ linear2.linear [linear]
143+
op inputs: {0: relu.relu[0]}
144+
op states: weight, bias
145+
op outputs: {0: [output_0]}
146+
filepath: my_model.py:18
147+
```
115148

116149
## Querying Operations by Config Key
117150

118-
Once you have reviewed the full summary to see what names and types are present, you can use query methods to check which operations would be matched by a specific name or type pattern. This is useful for verifying your config will target the intended ops before applying compression.
151+
Once you have reviewed the full summary to see what names and types are present, use the query methods to check which operations a specific pattern matches. This is useful for verifying that a config targets the intended ops before applying compression.
119152

120153
Each query method returns a tuple of {class}`~coreai_opt.inspection.OpInfo` objects matching the filter. The method names correspond directly to the config keys they help populate.
121154

122-
From the Basic Usage summary, this model exposes:
155+
From the graph mode summary above, this model exposes:
123156

124157
- **Op types**: `linear`
125158
- **Op names**: `linear`, `linear_1`
126159
- **Module types**: `torch.nn.modules.linear.Linear`, `torch.nn.modules.activation.ReLU`
127160
- **Module names**: `linear1`, `relu`, `linear2`
128161

129-
Op names and module names can be passed as a literal name or as a regex following [Python re syntax](https://docs.python.org/3/library/re.html) for wildcard matching; the pattern is matched against the entire string. The matching methodology is identical to how compression config entries match modules and ops in a model, allowing the user to see exactly which modules or ops would be matched given a particular string.
130-
131-
Each query method returns a tuple of {class}`~coreai_opt.inspection.OpInfo` objects matching the filter:
162+
Op names and module names can be passed as a literal string or as a regex following [Python re syntax](https://docs.python.org/3/library/re.html) for wildcard matching. The pattern is matched against the full string. The matching behavior is identical to how compression config entries match modules and ops, so you can see exactly which ops a given pattern would select.
132163

133164
**By op type** — exact-string match against `op_type_config` keys:
134165

@@ -151,10 +182,10 @@ inspector.get_matched_ops_for_module_name(
151182
) # matches the op in module "linear1"
152183
inspector.get_matched_ops_for_module_name(
153184
"linear[12]"
154-
) # matches ops in modules "linear1" and "linear2"
185+
) # matches ops in "linear1" and "linear2"
155186
```
156187

157-
Each returned {class}`~coreai_opt.inspection.OpInfo` provides `op_name`, `op_type`, and `module_stack` (the nesting of modules containing the op):
188+
Each returned {class}`~coreai_opt.inspection.OpInfo` provides `op_name`, `op_type`, `module_stack`, `inputs`, `outputs`, and `is_state`. The `module_stack` is a tuple of {class}`~coreai_opt.inspection.ModuleContext` entries from outermost to innermost module:
158189

159190
```python
160191
>>> for op in inspector.get_matched_ops_for_op_type("linear"):
@@ -166,6 +197,37 @@ Each returned {class}`~coreai_opt.inspection.OpInfo` provides `op_name`, `op_typ
166197
module: linear2 (torch.nn.modules.linear.Linear)
167198
```
168199

200+
`OpInfo.inputs` is a tuple of {class}`~coreai_opt.inspection.InputEdge` objects, one per input argument position. Each `InputEdge` carries the producing `OpInfo` and the output slot index (`output_idx`) of that producer. State inputs (parameters, buffers) are interleaved in the tuple at their actual argument positions, and their corresponding `InputEdge` objects have `is_state=True`.
201+
202+
Using these strings directly in a config:
203+
204+
```python
205+
config = QuantizerConfig(
206+
# Target a specific module by name
207+
module_name_configs={
208+
"linear1": ModuleQuantizerConfig(...),
209+
},
210+
# Target all modules of a given type
211+
module_type_configs={
212+
"torch.nn.modules.linear.Linear": ModuleQuantizerConfig(...),
213+
},
214+
)
215+
216+
# Op-level targeting within a ModuleQuantizerConfig
217+
config = QuantizerConfig(
218+
global_config=ModuleQuantizerConfig(
219+
# Target a specific op by name
220+
op_name_config={
221+
"linear_1": OpQuantizerConfig(...),
222+
},
223+
# Target all ops of a given type
224+
op_type_config={
225+
"linear": OpQuantizerConfig(...),
226+
},
227+
),
228+
)
229+
```
230+
169231
## Navigating the Module Hierarchy
170232

171233
For programmatic access to the inspector's data structures, the {class}`~coreai_opt.inspection.ModelSummary` exposes a {class}`~coreai_opt.inspection.ModuleInfo` tree that mirrors the `nn.Module` hierarchy. These types are publicly exported from `coreai_opt.inspection` for use in custom analysis or tooling.
@@ -180,11 +242,24 @@ linear2: torch.nn.modules.linear.Linear, 1 direct ops
180242
```
181243

182244
```python
183-
# look up a specific submodule
245+
# Look up a specific submodule
184246
linear2_module = root.get_submodule("linear2")
185247

186-
# get all ops under this subtree (depth-first)
248+
# Get all ops under this subtree (depth-first)
187249
linear2_ops = linear2_module.all_ops()
188250
```
189251

190252
`ModuleInfo` supports the same iteration patterns as `nn.Module`: `children()`, `named_children()`, `modules()`, `named_modules()`, and `get_submodule()`.
253+
254+
`ModuleInfo` also exposes the module boundary connectivity described in the tree:
255+
256+
- `input_ops` — dict mapping module input spec index to a list of {class}`~coreai_opt.inspection.BoundaryEdge` objects, each holding the op and input slot receiving data from outside the module.
257+
- `output_ops` — dict mapping module output spec index to a single {class}`~coreai_opt.inspection.BoundaryEdge`, holding the op and output slot whose tensor leaves the module.
258+
259+
```python
260+
# Inspect boundary connectivity for a submodule
261+
linear1_module = root.get_submodule("linear1")
262+
for idx, edges in linear1_module.input_ops.items():
263+
for edge in edges:
264+
print(f" module input {idx} -> {edge.op.op_name}[{edge.index}]")
265+
```

0 commit comments

Comments
 (0)