Skip to content

Commit 143d6e8

Browse files
Utkarsh Simhau-simha
authored andcommitted
Add graph mode debugging hints and troubleshooting doc
Surfaces actionable hints (export_with_no_grad toggle, dynamic_shapes, eager fallback) in the torch.export.export failure message, and includes the source module name when a graph partition annotation fails so it can be excluded via module_name_configs. Adds a Graph Mode Troubleshooting doc under a new Debugging doc section.
1 parent 4968b10 commit 143d6e8

9 files changed

Lines changed: 225 additions & 9 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Graph Mode Troubleshooting
2+
3+
This guide helps debug common issues when using Graph execution mode in CoreAI-Opt.
4+
5+
A `quantizer.prepare()` failure in graph mode happens in one of two stages, and the fix differs sharply between them. The first thing to do is figure out **which** stage is failing.
6+
7+
## Step 1: Diagnose — does `torch.export.export` succeed?
8+
9+
`Quantizer.prepare()` first calls `torch.export.export` to trace the model into an FX graph, then applies quantization annotations on the resulting graph and runs `torchao`'s `prepare_qat_pt2e`. To localize the failure, run the export step directly with the same arguments `prepare()` would use:
10+
11+
```python
12+
import torch
13+
14+
with torch.no_grad(): # matches export_with_no_grad=True (the prepare() default)
15+
exported_program = torch.export.export(model, example_inputs)
16+
```
17+
18+
The result of this experiment determines which path to follow:
19+
20+
- **Export fails.** Go to [If `torch.export.export` fails](#if-torch-export-export-fails). The model isn't `torch.export`-compatible as written; the workarounds in Steps 2-3 may help.
21+
- **Export succeeds but `prepare()` still fails.** Go to [If `prepare()` fails after a successful export](#if-prepare-fails-after-a-successful-export).
22+
23+
## If `torch.export.export` fails
24+
25+
### Step 2: Try `export_with_no_grad=False`
26+
27+
The default `export_with_no_grad=True` wraps the export call in `torch.no_grad()`. For some models, this context modifies tracing behavior and causes guard failures.
28+
29+
```python
30+
prepared = quantizer.prepare(
31+
example_inputs=(input_tensor,),
32+
export_with_no_grad=False,
33+
)
34+
```
35+
36+
### Step 3: Use dynamic shapes for shape-related errors
37+
38+
If the error mentions shape constraints, guards, or symbolic dimensions, the model likely has inputs with variable dimensions (e.g., sequence length, batch size). Specify `dynamic_shapes` to tell the exporter which dimensions can vary:
39+
40+
```python
41+
from torch.export.dynamic_shapes import Dim
42+
43+
# Example: dynamic batch dimension
44+
prepared = quantizer.prepare(
45+
example_inputs=(input_tensor,),
46+
dynamic_shapes={"x": (Dim.AUTO, Dim.STATIC, Dim.STATIC, Dim.STATIC)},
47+
)
48+
49+
# Example: dynamic sequence length with a max constraint
50+
import torch.export
51+
52+
dynamic_shapes = {
53+
"input_ids": {1: torch.export.Dim("seq_len", max=2048)},
54+
"attention_mask": {1: torch.export.Dim("seq_len", max=2048)},
55+
}
56+
prepared = quantizer.prepare(
57+
example_inputs=(input_ids, attention_mask),
58+
dynamic_shapes=dynamic_shapes,
59+
)
60+
```
61+
62+
For full details on dynamic shapes, see the [PyTorch Export Tutorial -- Dynamic Shapes](https://docs.pytorch.org/tutorials/intermediate/torch_export_tutorial.html#constraints-dynamic-shapes).
63+
64+
If Steps 2-3 don't resolve the export failure (e.g., the model has data-dependent control flow that `torch.export` cannot capture), see [Fall back to EAGER execution mode](#fall-back-to-eager-execution-mode) below.
65+
66+
## If `prepare()` fails after a successful export
67+
68+
After `torch.export.export` returns, `Quantizer.prepare()` applies coreai-opt's annotation pass and then calls into torch's `prepare_qat_pt2e` API. If the error you're seeing comes from `prepare_qat_pt2e` itself, it is a torch-side issue — refer to the [`torchao` documentation](https://docs.pytorch.org/ao/stable/) and report against torch.
69+
70+
If the error does **not** come from `prepare_qat_pt2e` (i.e. it originates inside coreai-opt's annotation pass), it likely indicates a bug on our end. **Please file an issue on GitHub** with the error message and a minimal reproducer. In the meantime, [fall back to eager mode](#fall-back-to-eager-execution-mode) below — eager bypasses the entire graph-mode pipeline.
71+
72+
## Fall back to EAGER execution mode
73+
74+
EAGER mode bypasses `torch.export` entirely and uses runtime tracing instead. It is the common fallback for both export failures that can't be worked around with Steps 2-3 and post-export `prepare()` failures.
75+
76+
```python
77+
from coreai_opt.quantization import ExecutionMode
78+
79+
config = QuantizerConfig.presets.w8()
80+
config.execution_mode = ExecutionMode.EAGER
81+
quantizer = Quantizer(model, config)
82+
prepared = quantizer.prepare(example_inputs=(input_tensor,))
83+
```
84+
85+
See [Choosing between graph and eager mode](../quantization/overview.md#choosing-between-graph-and-eager-mode) for the trade-offs between the two modes.
86+
87+
## External Resources
88+
89+
- [PyTorch Export Tutorial](https://docs.pytorch.org/tutorials/intermediate/torch_export_tutorial.html)
90+
- [Dynamic Shapes](https://docs.pytorch.org/tutorials/intermediate/torch_export_tutorial.html#constraints-dynamic-shapes)

docs/src/index.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,20 @@ palettization/index
3838
3939
utils/joint_compression
4040
utils/mixed_precision
41-
utils/model_inspection
4241
utils/activation_comparison
4342
utils/casting
4443
utils/coreai_compression
4544
```
4645

46+
```{toctree}
47+
:maxdepth: 1
48+
:caption: Debugging
49+
:hidden:
50+
51+
debugging/model_inspection
52+
debugging/graph_mode_troubleshooting
53+
```
54+
4755
```{toctree}
4856
:maxdepth: 1
4957
:caption: Reference

docs/src/quantization/config.md

Lines changed: 2 additions & 2 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 [Inspecting Model Structure](../utils/model_inspection.md).
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](../debugging/model_inspection.md).
188188

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

@@ -874,4 +874,4 @@ inspector = ModelInspector(
874874
print(inspector.format_summary())
875875
```
876876

877-
See [Inspecting Model Structure](../utils/model_inspection.md) for full usage, examples, and a comparison of graph and eager mode op naming.
877+
See [Inspecting Model Structure](../debugging/model_inspection.md) for full usage, examples, and a comparison of graph and eager mode op naming.

docs/src/quantization/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ The two modes are expected to produce very similar models for weight-only quanti
202202

203203
A few scenarios where `eager` mode may need to be used instead of `graph`:
204204

205-
- If you run into any errors during the `prepare` call which, under the hood, invokes the `torch.export.export` and `torchao`'s `prepare_qat_pt2e`/`convert_pt2e` APIs.
205+
- If you run into any errors during the `prepare` call which, under the hood, invokes the `torch.export.export` and `torchao`'s `prepare_qat_pt2e`/`convert_pt2e` APIs. See [Graph Mode Troubleshooting](../debugging/graph_mode_troubleshooting.md) for common export errors and workarounds before falling back to eager mode.
206206
- When `torch.nn.Module` needs to be provided as an input, instead of `ExportedProgram` to the conversion API of [coreai-torch](https://github.com/apple/coreai-torch). This happens when the `coreai-torch` conversion needs to "externalize" certain sub-modules to map them to _composite ops_ for better runtime performance.
207207

208208
#### Weights and activations quantization

src/coreai_opt/_utils/torch_utils.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -390,12 +390,13 @@ def export_model(
390390
Args:
391391
model (torch.nn.Module): The model to export.
392392
example_inputs (tuple[Any, ...]): Example inputs for tracing.
393-
dynamic_shapes: Dynamic shapes specification for torch.export.
393+
dynamic_shapes (dict[str, Any] | tuple[Any] | list[Any] | None):
394+
Dynamic shapes specification for torch.export.
394395
Can be a dict mapping input names to dynamic dimensions,
395396
a tuple/list of dynamic shapes per input, or None for
396397
static shapes. Used to specify which dimensions can vary
397398
at runtime during model export.
398-
export_with_no_grad: Whether to call torch.export.export within a
399+
export_with_no_grad (bool): Whether to call torch.export.export within a
399400
torch.no_grad() context.
400401
401402
Returns:
@@ -416,8 +417,27 @@ def export_model(
416417
exported_model = exported_program.module()
417418
return exported_model
418419
except Exception as e:
420+
no_grad_hint = (
421+
" - Try export_with_no_grad=False — the torch.no_grad() context can "
422+
"modify tracing behavior for some models.\n"
423+
if export_with_no_grad
424+
else " - Try export_with_no_grad=True — exporting with torch.no_grad() "
425+
"simplifies the traced graph.\n"
426+
)
427+
_dynamic_shapes_url = (
428+
"https://docs.pytorch.org"
429+
"/tutorials/intermediate/torch_export_tutorial.html"
430+
"#constraints-dynamic-shapes"
431+
)
419432
raise RuntimeError(
420-
f"Failed to trace the model with torch.export.export(), received error: {e}"
433+
f"Failed to trace the model with torch.export.export(), received error: "
434+
f"{e}\n\n"
435+
f"Debugging hints:\n"
436+
f"{no_grad_hint}"
437+
f" - If the error mentions shape constraints, try specifying "
438+
f"dynamic_shapes. See: {_dynamic_shapes_url}\n"
439+
f" - As a last resort, consider using EAGER execution mode: "
440+
f"config.execution_mode = ExecutionMode.EAGER"
421441
) from e
422442

423443

src/coreai_opt/quantization/_graph/_annotation_utils.py

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from coreai_opt._utils.version_utils import version_ge as _version_ge
4242
from coreai_opt.config.compression_config import ModuleConfigDict
4343
from coreai_opt.config.spec import CompressionTargetTensor
44+
from coreai_opt.quantization._graph._utils import get_source_module_name
4445
from coreai_opt.quantization.config import ModuleQuantizerConfig
4546
from coreai_opt.quantization.config.quantization_config import (
4647
_ACTIVATION_SPEC_DICT,
@@ -914,8 +915,31 @@ def _propagate_output_qspec(
914915

915916

916917
def _get_call_function_node_from_partition(partition: SourcePartition) -> torch.fx.Node:
917-
"""Return the first call_function node in the partition."""
918-
return [node for node in partition.nodes if node.op == "call_function"][0]
918+
"""
919+
Given a partition, return the call function node associated with the partition.
920+
921+
We expect there to be only one call function node in the partition.
922+
"""
923+
call_function_nodes = [node for node in partition.nodes if node.op == "call_function"]
924+
if len(call_function_nodes) != 1:
925+
module_names = {
926+
name
927+
for node in call_function_nodes
928+
if (name := get_source_module_name(node)) is not None
929+
}
930+
module_hint = ""
931+
if module_names:
932+
module_hint = (
933+
f"\nSource module(s): {', '.join(sorted(module_names))}. "
934+
f"Consider excluding this module from quantization via "
935+
f"module_name_configs."
936+
)
937+
error_msg = (
938+
f"Expected exactly 1 call function node in source partition but got "
939+
f"{call_function_nodes}.{module_hint}"
940+
)
941+
raise RuntimeError(error_msg)
942+
return call_function_nodes[0]
919943

920944

921945
def match_pattern_with_sequential_partitions(

tests/quantization/test_graph_mode_quantizer.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,20 @@ def test_prepare_with_dynamic_shapes(
183183
output = prepared_model(simple_model_input)
184184
assert output.shape == (1, 10)
185185

186+
def test_export_error_message_contains_hints(self, basic_config):
187+
"""Test that export failure error messages contain debugging hints."""
188+
189+
class DataDependentModel(nn.Module):
190+
def forward(self, x):
191+
if x.sum() > 0:
192+
return x * 2
193+
return x * 3
194+
195+
model = DataDependentModel()
196+
quantizer = Quantizer(model, basic_config)
197+
with pytest.raises(RuntimeError, match="Debugging hints"):
198+
quantizer.prepare(example_inputs=(torch.randn(2, 2),))
199+
186200
def test_finalize_with_none_model_arg(
187201
self, simple_conv_linear_model, basic_config, simple_model_input
188202
):

tests/quantization/test_graph_mode_utils.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@
44
# be found in the LICENSE file or at https://opensource.org/licenses/BSD-3-Clause
55

66
from functools import wraps
7+
from unittest.mock import MagicMock
78

9+
import pytest
810
import torch
911
import torch.nn as nn
1012

1113
from coreai_opt.quantization import Quantizer, QuantizerConfig
14+
from coreai_opt.quantization._graph._annotation_utils import (
15+
_get_call_function_node_from_partition,
16+
)
1217
from coreai_opt.quantization._graph._utils import (
1318
_has_no_disallowed_kwargs,
1419
restore_kwargs,
@@ -178,3 +183,58 @@ def test_strip_and_restore_round_trip(self):
178183

179184
restore_kwargs(graph, saved)
180185
assert custom_node.kwargs == metadata
186+
187+
188+
class TestGetCallFunctionNodeFromPartition:
189+
def test_single_node_returns_node(self):
190+
"""Single call_function node in partition is returned successfully."""
191+
node = MagicMock()
192+
node.op = "call_function"
193+
partition = MagicMock()
194+
partition.nodes = [node]
195+
196+
result = _get_call_function_node_from_partition(partition)
197+
assert result is node
198+
199+
def test_multi_node_error_includes_module_name(self):
200+
"""Multi-node partition error includes module name from nn_module_stack."""
201+
module_fqn = "encoder.layer.0.attention.self"
202+
nodes = []
203+
for _ in range(3):
204+
node = MagicMock()
205+
node.op = "call_function"
206+
node.meta = {"nn_module_stack": {"key": (module_fqn, type)}}
207+
nodes.append(node)
208+
209+
partition = MagicMock()
210+
partition.nodes = nodes
211+
212+
with pytest.raises(RuntimeError, match=module_fqn):
213+
_get_call_function_node_from_partition(partition)
214+
215+
def test_multi_node_error_suggests_module_name_configs(self):
216+
"""Multi-node partition error suggests using module_name_configs."""
217+
node = MagicMock()
218+
node.op = "call_function"
219+
node.meta = {"nn_module_stack": {"key": ("model.layer1", type)}}
220+
221+
partition = MagicMock()
222+
partition.nodes = [node, node]
223+
224+
with pytest.raises(RuntimeError, match="module_name_configs"):
225+
_get_call_function_node_from_partition(partition)
226+
227+
def test_multi_node_error_without_module_stack(self):
228+
"""Multi-node partition error still works without nn_module_stack."""
229+
nodes = []
230+
for _ in range(2):
231+
node = MagicMock()
232+
node.op = "call_function"
233+
node.meta = {}
234+
nodes.append(node)
235+
236+
partition = MagicMock()
237+
partition.nodes = nodes
238+
239+
with pytest.raises(RuntimeError, match="Expected exactly 1 call function node"):
240+
_get_call_function_node_from_partition(partition)

0 commit comments

Comments
 (0)