Skip to content

Commit d1e5d37

Browse files
authored
Add graph mode debugging hints and troubleshooting doc (#39)
* 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. * docs: fix stale model_inspection.md link in palettization config The link still pointed at the old utils/ location after this branch moved the doc to debugging/. * docs: address review feedback on graph mode troubleshooting guide Link "Graph execution mode" to the execution-modes section, note that export failures may warrant fixing the model definition (since the same construct can block coreai-torch conversion later), and drop the "our"/"report against torch" phrasing per review. * fix: don't error on SymInt-only multi-node partitions in annotation utils torch.export's insert_deferred_runtime_asserts synthesizes one SymInt mul per shape-runtime assertion under a shared torch_fn tag, collapsing several into a single SourcePartition. The new strict "exactly 1 call function node" check treated this as an error, regressing test_prepare_with_symint_mul_partition_collision (and CI). These SymInt nodes carry no tensor value to annotate, so picking any one of them is safe, matching the pre-existing lenient behavior for this case.
1 parent edd4720 commit d1e5d37

10 files changed

Lines changed: 235 additions & 10 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](../quantization/overview.md#two-execution-modes-graph-and-eager) 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), the model definition itself may need to change to become exportable — this is worth fixing at the source, since the same construct can also block conversion via [coreai-torch](https://github.com/apple/coreai-torch) later on. Otherwise, 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/).
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 in coreai-opt. **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/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, use {class}`~coreai_opt.inspection.ModelInspector` with `execution_mode="eager"` — see [Inspecting Model Structure](../utils/model_inspection.md). Palettization supports eager mode only.
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](../debugging/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: 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
@@ -203,7 +203,7 @@ The two modes are expected to produce very similar models for weight-only quanti
203203

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

206-
- 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.
206+
- 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.
207207
- 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.
208208

209209
#### 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
@@ -403,12 +403,13 @@ def export_model(
403403
Args:
404404
model (torch.nn.Module): The model to export.
405405
example_inputs (tuple[Any, ...]): Example inputs for tracing.
406-
dynamic_shapes: Dynamic shapes specification for torch.export.
406+
dynamic_shapes (dict[str, Any] | tuple[Any] | list[Any] | None):
407+
Dynamic shapes specification for torch.export.
407408
Can be a dict mapping input names to dynamic dimensions,
408409
a tuple/list of dynamic shapes per input, or None for
409410
static shapes. Used to specify which dimensions can vary
410411
at runtime during model export.
411-
export_with_no_grad: Whether to call torch.export.export within a
412+
export_with_no_grad (bool): Whether to call torch.export.export within a
412413
torch.no_grad() context.
413414
414415
Returns:
@@ -429,8 +430,27 @@ def export_model(
429430
exported_model = exported_program.module()
430431
return exported_model
431432
except Exception as e:
433+
no_grad_hint = (
434+
" - Try export_with_no_grad=False — the torch.no_grad() context can "
435+
"modify tracing behavior for some models.\n"
436+
if export_with_no_grad
437+
else " - Try export_with_no_grad=True — exporting with torch.no_grad() "
438+
"simplifies the traced graph.\n"
439+
)
440+
_dynamic_shapes_url = (
441+
"https://docs.pytorch.org"
442+
"/tutorials/intermediate/torch_export_tutorial.html"
443+
"#constraints-dynamic-shapes"
444+
)
432445
raise RuntimeError(
433-
f"Failed to trace the model with torch.export.export(), received error: {e}"
446+
f"Failed to trace the model with torch.export.export(), received error: "
447+
f"{e}\n\n"
448+
f"Debugging hints:\n"
449+
f"{no_grad_hint}"
450+
f" - If the error mentions shape constraints, try specifying "
451+
f"dynamic_shapes. See: {_dynamic_shapes_url}\n"
452+
f" - As a last resort, consider using EAGER execution mode: "
453+
f"config.execution_mode = ExecutionMode.EAGER"
434454
) from e
435455

436456

src/coreai_opt/quantization/_graph/_annotation_utils.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from coreai_opt._utils.version_utils import version_ge as _version_ge
4141
from coreai_opt.config.compression_config import ModuleConfigDict
4242
from coreai_opt.config.spec import CompressionTargetTensor
43+
from coreai_opt.quantization._graph._utils import get_source_module_name
4344
from coreai_opt.quantization.config import ModuleQuantizerConfig
4445
from coreai_opt.quantization.config.quantization_config import (
4546
_ACTIVATION_SPEC_DICT,
@@ -948,8 +949,40 @@ def _propagate_output_qspec(
948949

949950

950951
def _get_call_function_node_from_partition(partition: SourcePartition) -> torch.fx.Node:
951-
"""Return the first call_function node in the partition."""
952-
return [node for node in partition.nodes if node.op == "call_function"][0]
952+
"""
953+
Given a partition, return the call function node associated with the partition.
954+
955+
We expect there to be only one call function node in the partition.
956+
"""
957+
call_function_nodes = [node for node in partition.nodes if node.op == "call_function"]
958+
if len(call_function_nodes) != 1:
959+
# torch.export's insert_deferred_runtime_asserts synthesizes one SymInt mul per
960+
# shape-runtime assertion, all sharing one torch_fn tag, so several can collapse
961+
# into a single partition. They carry no tensor value to annotate, so picking any
962+
# one of them is safe here; downstream floating-point filtering no-ops on SymInt.
963+
if call_function_nodes and all(
964+
isinstance(node.meta.get("val"), torch.SymInt) for node in call_function_nodes
965+
):
966+
return call_function_nodes[0]
967+
968+
module_names = {
969+
name
970+
for node in call_function_nodes
971+
if (name := get_source_module_name(node)) is not None
972+
}
973+
module_hint = ""
974+
if module_names:
975+
module_hint = (
976+
f"\nSource module(s): {', '.join(sorted(module_names))}. "
977+
f"Consider excluding this module from quantization via "
978+
f"module_name_configs."
979+
)
980+
error_msg = (
981+
f"Expected exactly 1 call function node in source partition but got "
982+
f"{call_function_nodes}.{module_hint}"
983+
)
984+
raise RuntimeError(error_msg)
985+
return call_function_nodes[0]
953986

954987

955988
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
@@ -184,6 +184,20 @@ def test_prepare_with_dynamic_shapes(
184184
output = prepared_model(simple_model_input)
185185
assert output.shape == (1, 10)
186186

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

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)