Debugging C7x Compiler Passes
This guide covers tools and techniques for debugging Relax and TIR passes in the C7x backend — from IR inspection to runtime verification.
IR Dumping Flags
--dump-ir (Relax)
Prints the Relax IRModule at each pass. Usage:
import tvm
from tvm import relax
target = tvm.target.Target("c_static_lib -mcpu=c7x -mmalib=1")
mod = relax.transform.LegalizeOps()(mod)
# Enable IR dumping
with tvm.transform.PassContext(opt_level=3, config={"relay.dump_ir": True}):
ex = relax.build(mod, target=target)
Or via environment variable:
TVM_LOG_DEBUG=1 python compile_model.py 2>&1 | grep -A 50 "After FuseMMALIBQDQConv2d"
Pass-Specific Dumping
Individual passes can dump their input/output IR by adding logging:
# In a pass's transform_module() or apply() method
import logging
logger = logging.getLogger("TVM.pass.my_pass")
logger.setLevel(logging.DEBUG)
logger.debug("Input module:\n%s", mod.script())
# ... pass logic ...
logger.debug("Output module:\n%s", mod.script())
Then run with:
TVM_LOG_DEBUG=pass.my_pass python compile_model.py
TIR Dumping
After FuseTIR lowers to TIR, dump TIR PrimFuncs:
# In TIR pass
func = ... # tir.PrimFunc
print(func.script()) # Human-readable TIR
# Or with annotations
print(tvm.script.printer.ir_printer.IRPrinter()(func))
TVM_LOG_DEBUG Categories
Key debug categories for C7x development:
| Category | What It Logs |
|---|---|
TVM.pass.fuse_ops_by_pattern |
Pattern matches, composites created, decline reasons |
TVM.pass.mmalib_qdq_fusion |
QDQ pattern matches, eligibility check results, folded params |
TVM.pass.mmalib_inject_dma |
DMA injection decisions, buffer sizes, L2 budget checks |
TVM.pass.schedule_c7x_dma |
Tiling strategy chosen, tile sizes, software pipeline annotations |
TVM.tir.lower_dma_to_extern |
DMA intrinsic → call_extern conversions |
TVM.codegen.c_static |
Function emission, wrapper generation, symbol resolution |
Enable multiple:
TVM_LOG_DEBUG="TVM.pass.mmalib_qdq_fusion,TVM.pass.mmalib_inject_dma" python compile_model.py
Inspecting Composite Functions
After FuseOpsByPattern
Composites appear as GlobalVar with "Composite" attribute:
# In a PyExprMutator lowerer
def visit_call_(self, call):
if isinstance(call.op, relax.GlobalVar):
gv = call.op
func = self.builder_.get()[gv]
if "Composite" in func.attrs:
composite_name = func.attrs["Composite"]
print(f"Found composite: {composite_name}")
print(f"Composite body:\n{func.script()}")
After FuseTIR
Composites are lowered to PrimFunc with global_symbol matching the
composite name. Check mod.functions for TIR functions.
Decline Branches: inline_declined_composite
If a pass's check function returns False, the matched call is not
automatically consumed — it remains as a Call to the composite GlobalVar.
FuseTIR's TIRFuseMutator fuses every Primitive-tagged GlobalVar
still in the module regardless of whether anything still calls it, so a
composite left un-consumed must legalize and fuse cleanly entirely on its own.
Defense: call the shared inline_declined_composite helper
(ti_c7x_composite_inline.py) from the decline branch, then run
relax.transform.DeadCodeElimination()(mod) afterward so the now-orphaned
composite function is actually deleted. Landed in ti_fuse_qdq_c7x_movement.py
and ti_fuse_qdq_c7x_relu.py (commit b64d0fa3bf).
Not a demonstrated crash on this tree: bind_constants=False lifts
matched constants to composite parameters rather than embedding them in the
body, and a declined composite left in place has been measured to compile
cleanly through LegalizeOps/FoldConstant/FuseOps/FuseTIR (corrected
rationale in ti_c7x_composite_inline.py, commit d1c0f41f27). If you hit
FuseTIR error "Relax.Constant is not supported in primitive functions",
capture the stack trace first — the one demonstrated cause on this tree was
an unrelated stale parameter index in fuse_ops.cc's tuple-parameter
splicing (6fe9a33f09), not a decline branch. See
C Static Lib Backend
for the distinct, actually-demonstrated decline-related hazard
(ConstReachability, be39717c39).
DSP_KEEP_TEMP: Preserving Generated Artifacts
Set DSP_KEEP_TEMP=1 to preserve the generated lib0.c, lib1.c,
weights.bin, and intermediate build artifacts:
DSP_KEEP_TEMP=1 python compile_model.py
# Artifacts in /tmp/tvm_<pid>/
Finding Unfused QDQ Nodes
Search for unfused quantize/dequantize in generated C:
grep -n "quantize_per_tensor\|dequantize" /tmp/tvm_*/lib0.c
Each call site is an unfused QDQ boundary — overhead that a fusion pass should have eliminated.
Finding DMA Injection
grep -n "tvm_dsp_dma_copy\|tvm_dsp_dma_wait" /tmp/tvm_*/lib0.c
Shows which kernels got L2 SRAM staging.
Layer Profiling
Compile with -profile-layers=1 and run on hardware/emulation:
target = tvm.target.Target("c_static_lib -mcpu=c7x -profile-layers=1")
Output (via DSP printf to shared memory buffer):
[LAYER] conv2d_0: 1245832 cycles
[LAYER] relu_0: 12345 cycles
[LAYER] mmalib_conv2d_i8: 477234 cycles
[LAYER] quantize_per_tensor: 89234 cycles <-- UNFUSED QDQ OVERHEAD
Look for quantize_per_tensor / dequantize entries — their cycle counts
are pure overhead that a fusion pass would eliminate.
Common Debugging Scenarios
Pass Not Firing
-
Check pattern specificity — Is the DFPattern too strict/loose? Add logging in check function to see what's matched vs rejected.
-
Check pass ordering — MMALIB passes must run BEFORE
FuseQDQToInt8Conv2DandEliminateQDQRoundTrip. Verify inpipeline.py. -
Check bind_constants —
bind_constants=Falselifts matched constants out to composite parameters (call-site arguments);bind_constants=True(default) leaves them bound inside the composite body. The C7x/MMALIB QDQ passes all useFalseand recover values via_resolve_constant_tensor. Separately, a 0-d scalar constant (e.g. a per-tensor scale) represented as a TE tensor inside ate.compute/te.externcallback is indexed asscale[()], notscale[indices[axis]]— seefuse_dequantize_matmul.py. -
Check constant resolution — PT2E wraps bias in
reshape(bias, (1,C,1,1)). Use_resolve_constant_tensorto unwrap.
Incorrect Codegen
-
Check te.extern name vs call_extern symbol — Must be different! Same name creates local variable shadowing function declaration.
-
Check extern symbol matches firmware export — Symbol must be in
dyn_loader.cexport table exactly. -
Check parameter order —
call_externargs must match wrapper signature.
Runtime Failures
-
DLOAD symbol resolution error — Missing symbol in firmware export table. Check
dyn_loader.cfor the symbol. -
MMALIB error codes — Wrapper returns MMALIB status. Check:
-1= NULL pointer argument-
MMALIB_ERR_NOT_IMPLEMENTED= unsupported geometry (e.g., int16 5×5 dwconv) -
Numerical mismatch — Compare against PyTorch reference:
pytest --rootdir=. pt2e-tests/test_c7x_mma_quantizer_e2e_dsp.py::test_e2e_conv2d_i8 -v --dsp-mode=c7x_dload
Debugging TIR Passes
Print TIR Before/After Pass
# In TIR pass transform function
def transform(mod, ctx):
for gv, func in mod.functions.items():
if isinstance(func, tir.PrimFunc):
print(f"=== BEFORE {pass_name} ===")
print(func.script())
# ... pass logic ...
for gv, func in mod.functions.items():
if isinstance(func, tir.PrimFunc):
print(f"=== AFTER {pass_name} ===")
print(func.script())
return mod
Key TIR Passes to Inspect
| Pass | What to Check |
|---|---|
InjectMMALIBDMA |
L2 Allocate nodes added, DMA copy/wait inserted, guard allocation present |
LowerL2SramAlloc |
Allocate(scope="global.l2sram") → tvm_l2_alloc calls |
LowerDMAToExtern |
tir.dma_copy/tir.dma_wait → call_extern("tvm_dsp_dma_copy", ...) |
LowerAsyncDMA |
Copy loops → tir.dma_copy intrinsics (requires int64 indices) |
ScheduleC7xDMATiling |
cache_read into global.l2sram, software pipeline annotations |
Software Pipeline Annotations
Check for these attributes on the outer loop:
# Expected annotations on tiled loop
attrs = {
"software_pipeline_stage": [0, 0, 1], # DMA, DMA, compute
"software_pipeline_order": [0, 1, 2],
"software_pipeline_async_stages": [0],
}
Debugging Tips
-
Start with unit tests —
pt2e-tests/test_c7x_mma_quantizer.pyruns without hardware -
Use host emulation first —
--dsp-mode=c7x_hostruns on x86 with memcpy DMA -
Compare IR at each stage — Dump after each pass in the pipeline
-
Check pass dependencies —
CanonicalizeBindings+DCEmust run before QDQ passes to clean up PT2E tuple artifacts -
Verify constant folding — All scale/shift/bias math should be compile-time. No float ops should reach the generated C for fused kernels.
-
Check
bind_constantsisFalse— If constants aren't folding, verify the pattern usesbind_constants=Falseso matched constants are lifted to composite call-site arguments (where_resolve_constant_tensorexpects to find them), not left bound inside the composite body.
Related Files
| File | Purpose |
|---|---|
python/tvm/relax/transform/ti_mmalib_passes.py |
Central registry, pass ordering |
python/tvm/relax/backend/cpu_generic/pipeline.py |
Full pipeline wiring |
python/tvm/tir/pipeline.py |
TIR pipeline (InjectMMALIBDMA, ScheduleC7xDMATiling) |
python/tvm/relax/transform/ti_mmalib_inject_dma.py |
DMA injection logic |
python/tvm/relax/transform/ti_mmalib_qdq_fusion.py |
Example pass with logging |
tests/ti-dsp-runtime/pt2e-tests/ |
Quantizer and E2E test patterns |