Debugging¶
Find NaN/infinity issues, compare model implementations, and analyze structural differences with the debugging module.
Note
During the current preview, set the following environment variables to ensure operation-level debug metadata is preserved and available to these tools:
export USE_LOCAL_COREAI=1
export ENABLE_DEBUG_INFO=1
Quick start¶
from coreai_torch.debugging.validator import create_validator_for_exported_program
# Find NaN/inf issues in PyTorch models
model = MyModel().eval()
exported = torch.export.export(model, args=(torch.randn(1, 10),))
validator = create_validator_for_exported_program(exported)
result = await validator.check_for_nans(inputs=(torch.randn(1, 10),))
if result.failed_nodes:
print(f"NaN detected at: {result.failed_nodes[0]}")
Finding NaN/infinity issues¶
Use when: Your model produces NaN or infinity values and you need to find which operation caused the issue.
PyTorch models¶
from coreai_torch.debugging.validator import create_validator_for_exported_program
# Export your model
exported_program = torch.export.export(model, args=example_input)
# Create validator
validator = create_validator_for_exported_program(exported_program)
# Check for numerical issues
nan_result = await validator.check_for_nans(inputs=example_input)
inf_result = await validator.check_for_infs(inputs=example_input)
# Get first failing operation
if nan_result.failed_nodes:
print(f"First NaN at: {nan_result.failed_nodes[0]}")
Core AI programs¶
from coreai_torch.debugging.validator import create_validator_for_coreai_program
# Convert to Core AI
converter = TorchConverter().add_exported_program(exported_program)
coreai_program = converter.to_coreai()
# Create validator
validator = await create_validator_for_coreai_program(coreai_program, "main")
# Check for issues
result = await validator.check_for_nans(inputs={"x": torch.randn(2, 4)})
Comparing model implementations¶
Use when: You need to verify that PyTorch and Core AI models produce the same outputs after conversion.
Cross-framework comparison¶
Compare PyTorch vs Core AI to verify conversion correctness:
from coreai_torch.debugging.comparator import create_comparator_for_programs
# Create comparator between PyTorch and Core AI
comparator = await create_comparator_for_programs(
source_program=exported_program,
target_program=coreai_program,
target_entry_point="main",
)
# Compare outputs with tolerance
result = await comparator.compare_with_tolerance(
inputs={"x": example_input}, rtol=1e-5, atol=1e-8
)
# Check for differences
if result.failed_nodes:
for source_op, target_op in result.failed_nodes:
print(f"Mismatch: {source_op} vs {target_op}")
Core AI inspector¶
Use when: You need to examine intermediate values from specific operations in a deployed Core AI model.
Capture intermediate values from deployed Core AI models:
from coreai_torch.debugging.inspector import CoreAIInspector
from coreai.runtime import AIModel
# Load deployed Core AI model
asset_path = Path("my_model.aimodel")
ai_model = await AIModel.load(asset_path)
# Create inspector
inspector = CoreAIInspector(model=ai_model, function_name="main")
# Get operation IDs to inspect (from debug info)
coreai_op_ids = [1, 5, 10, 15]
# Capture intermediate values
results = await inspector.get_intermediates_for_ops(
coreai_op_ids, inputs={"x": np.random.randn(2, 4).astype(np.float32)}
)
# Check results
for op_id, outputs in results.items():
print(f"Op {op_id}: {len(outputs) if outputs else 0} outputs")
Structural graph analysis¶
Use when: You want to understand how model structure changes between different versions or after optimization passes.
Graph difference analysis¶
Analyze structural differences between model implementations using graph isomorphism:
from coreai_torch.debugging.graph_diff import (
compute_exported_program_diff,
compute_coreai_program_diff,
write_diff,
)
# Compare two PyTorch programs
source_program = torch.export.export(model_v1, example_input)
target_program = torch.export.export(model_v2, example_input)
diff = compute_exported_program_diff(source_program, target_program)
# Check structural compatibility
if diff.is_isomorphic:
print("✓ Graphs have identical structure")
else:
print(f"✗ Found {diff.summary.unmapped_source_node_count} structural differences")
# Write detailed diff report to stdout
write_diff(diff, diff.source_graph, diff.target_graph, max_items=20)
Which layer changed¶
Use when: A structural diff told you something changed and you need to know where — which layer, and which line of your model.
compute_changes takes the same two programs and reports each differing operation
against the module instance and source line it came from, so a diff of thousands of
operations reads as a handful of layers:
from coreai_torch.debugging.changes import compute_changes
result = compute_changes(before_program, after_program)
# Totals plus per-module counts, hotspot first.
result.write_summary()
9 change(s): 7 added, 0 removed, 2 modified
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┳━━━━━━━┓
┃ Module ┃ Added ┃ Removed ┃ Modified ┃ Total ┃ Share ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━╇━━━━━━━┩
│ ExtraLayerModel$1/Linear$3 │ 6 │ 0 │ 0 │ 6 │ 67% │
│ ExtraLayerModel$1/Linear$4 │ 0 │ 0 │ 2 │ 2 │ 22% │
│ ExtraLayerModel$1 │ 1 │ 0 │ 0 │ 1 │ 11% │
└────────────────────────────┴───────┴─────────┴──────────┴───────┴───────┘
Module names identify instances (Linear$3, not Linear), so two structurally
identical layers are told apart — which operation names cannot do.
Then drill into one layer once you know which to ask about:
for change in result.changes_in("ExtraLayerModel$1/Linear$4"):
where = change.attribution.source
print(f"{change.change.value}: {change.op_name} at {where.filename}:{where.line}")
A module matches everything under it, so "Block$2" includes "Block$2/Linear$1".
For a programmatic caller, to_dict() gives the same report as plain values — counts
first, then the module tree with each change nested under the module it happened in:
import json
print(json.dumps(result.to_dict(), indent=2))
Note
Attribution comes from the stack traces TorchConverter.Mode.DEBUG records, which is
the default mode. Converted with stack traces off, changes are still reported but every
one files under <unknown>.
Grouping any finding by module¶
compute_changes is one consumer of a general rollup. Any tool holding op ids can name
the layer they came from:
from coreai_torch.debugging.modules import attributions_by_op_id, build_module_tree
attributions = attributions_by_op_id(program)
print(attributions[12].label) # 'Model$1/Block$2/Linear$1'
print(attributions[12].source) # LocationInfo(filename='model.py', line=41, col=8)
# Group your own findings the same way every report does.
tree = build_module_tree(
(attributions[op_id].module, finding) for op_id, finding in my_findings.items()
)
for root in tree:
print(root.name, len(root.all_items()))
Performance profiling¶
Use when: You need to identify slow operations and performance bottlenecks in your Core AI model.
Profile operation timing in Core AI programs:
from coreai_torch.debugging.benchmarker import benchmark_coreai_program
# Run benchmark
result = await benchmark_coreai_program(
coreai_program=coreai_program, inputs={"x": torch.randn(2, 4)}, num_runs=50
)
# Show timing summary
result.write_summary(sys.stdout)
# Get module-level timing
module_timings = result.get_module_timings()
for name, module in module_timings.items():
print(f"{name}: {module.aggregated_op_stats.average:.3f}ms avg")
Every report groups the same way¶
by_module() is on the comparison reports too, so “which layer” is one call whatever
you were measuring — and always the same shape:
# Which layer moved off a device
diff = await compare_compute_plans(before_program, after_program)
for node in diff.by_module():
print(f"{node.name}: {len(node.all_items())} operation(s) moved")
# Which layer diverges numerically
report = await compare_program_intermediates(exported_program, program, inputs)
for node in report.by_module():
print(f"{node.name}: {len(node.all_items())} failing")
# Which layer changed structurally
changes = compute_changes(before_program, after_program)
changes.write_summary()
Each entry also carries its own module path, so you can group or filter without the
tree:
for entry in report.failures:
print("/".join(entry.module), entry.max_diff)
Note
ComputePlanDiff.by_module() covers moves, and IntermediatesReport.by_module()
covers failures — the entries each report exists to draw attention to. Additions and
removals are compute_changes’ question, and a report over a real model is mostly
passing entries that would bury the handful that matter. Group changes or
comparisons yourself for the full picture.
Custom validation¶
Use when: You need to check for specific conditions beyond NaN/infinity (e.g., value ranges, specific patterns).
Create custom checks beyond NaN/infinity:
def check_large_values(outputs):
"""Check if any output has values > threshold"""
return any(abs(arr).max() > 1000.0 if arr is not None else False for arr in outputs)
# Use custom check
result = await validator.check(check_large_values, inputs=example_input)
Configuration¶
Search strategies¶
Choose how to search through operations:
from coreai_torch.debugging.search_strategy import (
ExhaustiveStrategy,
LevelOrderStrategy,
)
# Exhaustive (default) - one batch, one model execution per side, reports every issue
strategy = ExhaustiveStrategy(graph)
# Bisection - narrows by depth to the first failing level
strategy = LevelOrderStrategy.bisection(graph, batch_size=10)
# Top-down (systematic from inputs to outputs)
strategy = LevelOrderStrategy.top_down(graph)
# Adaptive (automatically selects best approach)
strategy = LevelOrderStrategy.auto(graph)
Every batch a strategy yields costs a full model execution on both sides, so
narrowing only pays when capturing a value is more expensive than a run — very large
intermediates, or an early exit that skips most of the graph. Otherwise the default
ExhaustiveStrategy is both cheaper and more complete.
Batch size¶
# Control batch size for memory efficiency
strategy = LevelOrderStrategy.bisection(graph, batch_size=5) # Smaller batches
strategy = LevelOrderStrategy.bisection(graph, batch_size=20) # Larger batches
validator = create_validator_for_exported_program(exported)
Torch utilities¶
Use when: You need to save intermediate values to disk for later analysis or share debug data.
Saving intermediate values¶
Save all intermediate tensor values from PyTorch model execution:
from coreai_torch.debugging.torch_utils import save_intermediates, load_intermediates
from pathlib import Path
# Export your PyTorch model
exported_program = torch.export.export(model, args=example_input)
# Save intermediate values to disk
metadata_path = save_intermediates(
program=exported_program, inputs=example_input, output_dir=Path("./debug_output")
)
print(f"Intermediates saved to: {metadata_path}")
Loading intermediate values¶
Load saved intermediate values for analysis:
# Load intermediate values from disk
debug_trace = load_intermediates(Path("./debug_output/main.aimodelintermediates"))
# Access saved values
print(f"Inputs: {list(debug_trace.inputs.keys())}")
print(f"Outputs: {list(debug_trace.outputs.keys())}")
print(f"Intermediates: {len(debug_trace.intermediates)} operations")
# Analyze specific intermediate values
for node_name, tensor in debug_trace.intermediates.items():
print(f"{node_name}: shape {tensor.shape}, mean {tensor.mean():.3f}")
Custom value filtering¶
Filter which intermediate values to save:
def custom_filter(node, result):
"""Only save convolution and linear layer outputs"""
return any(op in str(node.target).lower() for op in ["conv", "linear", "matmul"])
# Save only filtered operations
metadata_path = save_intermediates(
program=exported_program,
inputs=example_input,
output_dir=Path("./debug_output"),
node_filter=custom_filter,
)
The debugging module provides tools for validating model correctness, analyzing structural changes, and identifying performance issues.
See also¶
Conversion Workflows — understand the full pipeline from export to
DeployableProgram.Supported ATen ops — check which ATen ops have built-in lowering rules if you hit a conversion error.
TorchConverter API reference — the main conversion class;
to_coreai()produces the program you validate here.