Repository navigation
Add debug symbols that map generated code to the .stmt, and link the html outputs through them - #9611
mcourteaux wants to merge 2 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9611 +/- ##
==========================================
+ Coverage 73.01% 73.41% +0.39%
==========================================
Files 263 263
Lines 82383 82647 +264
Branches 20149 20416 +267
==========================================
+ Hits 60151 60672 +521
+ Misses 15915 15538 -377
- Partials 6317 6437 +120 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Why does this need three new Target features as opposed to just extending |
Because debug is extremely chatty when enabled on the runtime. It logs every gpu kernel launch. And AFAIK, you cannot combine a non-debug runtime with a debug pipeline. |
|
|
||
| // The Stmts a Stmt is directly made of, in order. | ||
| std::vector<Stmt> stmt_children(const Stmt &s) { | ||
| switch (s->node_type) { |
There was a problem hiding this comment.
I think this should use visit_with, to catch new IR nodes as they are added
| CPlusPlusFunctionInfoHeader, | ||
| }; | ||
|
|
||
| /** While recording where Stmts are printed (see IRPrinter), have a Stmt |
There was a problem hiding this comment.
I don't understand this comment. Can you rephrase?
| stream << get_indent() << "halide_maybe_unused(" << name << ");\n"; | ||
| } else { | ||
| Expr new_var = Variable::make(op->value.type(), id_value); | ||
| body = substitute(op->name, new_var, body); |
There was a problem hiding this comment.
This looks like it could be much simpler if there's a pre-pass that renames lets as needed instead of doing it on demand. Then we wouldn't need the replacement map at all (I think?)
| // support predication. | ||
| Stmt original = s; | ||
| s = scalarize_predicated_loads_stores(s); | ||
| d3d12compute_c.remember_replaced(original, s); |
There was a problem hiding this comment.
Alternatively, would it be simpler if there were a stmt node Debug that attached .stmt line numbers to the child .stmt? It seems like a lot of this complexity is because you need to store the debug info in a separate map instead of it being in the IR.
|
This looks like a great feature! Alex and I were discussing if a new IR node type or field on Stmt would make the implementation easier/simpler. I personally lean towards a new IR node. Would that work? I tagged the PR with dev_meeting so we can discuss it there at the latest, but we can also discuss it here via text. We also think it would be better for this to be a single target feature that turns on all three, but maybe we're missing something? |
|
Separately, this made us wonder if conceptual_stmt shouldn't just replace .stmt at this point. I don't ever use the latter - do you find both useful? |
I don't find it useful, but it's what we hand to LLVM, and so debug information is derived from there. This should give the best assembly-to-source mapping. However, when you propose to introduce a new IR node type that holds debug location info, than that becomes a lot simpler, and we should get equally good mapping into the conceptual STMT.
I think a lot of things become simpler if we can just annotate the nodes with debug info. Unfortunately, we have already established that IR nodes should be kept small. But implementation-wise, just adding a 20 bits of line info and 12 bits of column info seems like a good 32-bit cost for every node type without needing to do any IR node wrapping. I think the "debug" node type would just double the IR node tree size for no good reason. We have line:col info for every IR node in the final printed stmt, so a field seems the appropriate tool if you ask me.
If we remove the final stmt in favor the conceptual stmt, it is clearly possible to also remove one target feature. The reason I had it split out into two features is because line-to-assembly mapping is useful to go into gdb/lldb and often desirable for a very limited cost (emitting the source map info in the object file / dSYM). The GPU-source-mapping is much more expensive unfortunately: Claude thought it was a good idea to inject |
|
The point in favor of the IR node is that it's zero cost right up to the end of lowering where it gets injected, so intermediate lowering states where stmts might be very large (e.g. before simplifications) don't pay anything. Even more important, it's zero cost in non-debug builds. |
Given you say this, I should point out that the PR as is computes source-mappings even without any feature flag enabled. It does it if you save HTML version of stmt to inject the links. The target feature is to actually emit the debug symbols into the generated object file / static library. If we are concerned about IRNode cost, we should probably delete the vtable pointer. |
|
In non-debug builds, the PR only computes the mappings if you ask for html output, correct? |
No, it produces the mappings always, because the code in Lowering doesn't know what outputs will be required. If HTML is part of them, it would need to run through lowering again to get to the PTX offload, and then somehow do something different. To get a complete picture, I benchmarked lowering and LLVM backend of this PR against main:
Front-end only (
So it's basically noise. No significant cost is paid for telling LLVM about the debug source locations. |
|
Surely lowering could know whether or not it needs stmt source locations with an extra flag? I think we could afford to make it a field on Stmt. Most of the memory savings of removing the vtable pointer would have come from Expr. However, an issue with making it a field though is that currently the same Stmt node can show up in multiple places in the IR (e.g. due to unrolling or specialization), and this would force them to be unique. A new IR node would do the same thing though, except for leaf stmts. Let's discuss at the dev meeting. |
…html outputs through them The debug_symbols target feature gives host code DWARF (CodeView on Windows) debug info that points into the .stmt output, so a debugger can step through the lowered IR; debug_symbols_conceptual points it into the .conceptual.stmt instead. IRPrinter can record where it prints each IR node (print_with_source_map); the text is the same as that of operator<<. The html outputs link each line of IR to the assembly generated for it through the same debug info: their assembly is compiled with it even without the debug symbols features, and the other outputs then get none. The html outputs and the debug symbols find where code came from by the IR node it was made from, which must then be printed in one place only. With the debug symbols features, or when the Module will be compiled to an html output, which compile_to_module and generators are now told with a for_html_outputs flag, lowering rebuilds every IR node of the conceptual stmt so that none of them is used in more than one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TxSYmva6vWHCraKgSEe6Cu
f6546e5 to
f67d10d
Compare
…vm-debug-symbols Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TxSYmva6vWHCraKgSEe6Cu
Second of a stack of three PRs: stacked on #9690 (the html outputs print the IR with
IRPrinter), and followed by #9691 (GPU device code, draft). This PR was rebuilt after review: the GPU device code mapping moved to #9691, and the html printing to #9690.This adds debug info that maps generated code back to the printed Halide IR, so you can step through a pipeline's
.stmtin gdb/lldb, and links the html outputs to the assembly line by line through that same debug info. Seedoc/DebugSymbols.md.Target features
debug_symbols: host code gets DWARF (CodeView on Windows) debug info that points into the.stmtoutput. The pipeline is still optimized as usual.letvariables, loop variables and function arguments are visible to the debugger, and buffer arguments are typed ashalide_buffer_t *. JIT code is registered with the debugger through the GDB JIT interface (also with a statically linked LLVM).debug_symbols_conceptual: the same, but pointing into the.conceptual.stmt(the IR before offloading, closures and so on). Code that isn't in the conceptual stmt is attributed to the nearest enclosing IR that is. A target can't have both.Source maps
IRPrintercan record where it prints each IR node (print_with_source_map), using thebegin_node/end_nodehooks of #9690 and ananchor()for the column of operators, variables and calls. The text is exactly that ofoperator<<. The conceptual stmt and the stmt share the nodes lowering didn't rewrite, whichmap_locationsuses to carry locations from one to the other.The html outputs
The
stmt_htmlandconceptual_stmt_htmloutputs link each line of IR to the assembly generated for it, through the debug info in the assembly, instead of only loops by the names of their basic blocks (#9690). Their assembly is compiled with debug info even without the debug symbols features; it is then stripped from the other outputs, so those are the same as without an html output. The html shows the same text as the stmt files, so the line numbers in the debug info are those of the html.Each IR node printed once
Debug info and the html find where code came from by the IR node it was made from. A node that the IR uses in more than one place (shared subexpressions, the copies of an unrolled loop, the checks of the buffers) is printed more than once, and then it is ambiguous which copy code came from. So, with the debug symbols features, or when the Module will be compiled to an html output, lowering rebuilds every node of the conceptual stmt right before it is kept (
copy_shared_nodes, amutate_withthat rebuilds the nodes without IR in them, which forces all others to be rebuilt too).Lowering can't see which outputs will be requested, so
Pipeline::compile_to_module,AbstractGenerator::build_moduleandlower()get afor_html_outputsflag (defaultfalse), whichcompile_toand the generators set when an html output is requested. Without it (and without the debug features), nothing of this PR runs while lowering.Checked: with this stack, all 73 app generators compile to byte-identical objects with and without an html output, on host and host-cuda. (That needs a fix for an unrelated address-dependence of loop partitioning on main, which makes some apps lower differently depending on what else the process allocated, e.g. which outputs are requested; that fix will be a separate PR.)
Compile time
Compile time of all 73 app generators to an object file (
-e o: lowering, GPU kernels and LLVM codegen of the host code), mean of 3 runs, latest main against the top of this stack (#9691), both RelWithDebInfo with LLVM 22, without debug features or html outputs. "GPU" generators are the ones that launch kernels on that target.All within the noise (the per-generator ratios have a standard deviation below 1%): without the debug features or an html output, none of the stack's work runs.
Screenshots
The
conceptual_stmt_htmlof a blur with a line of IR clicked: the vector instructions generated for it are highlighted.Tests
correctness_debug_symbolschecks the DWARF of host code (locations point at printed IR, one subprogram per function, variables and buffers), CodeView/Apple/ARM assembly, the JIT,debug_symbols_conceptual, that the html outputs show exactly the stmt text and link lines to assembly, and that no IR node is printed twice in the conceptual stmt when lowered for html.Checklist
doc/DebugSymbols.md,compile_to_module,build_module)compile_to_module(..., for_html_outputs=))🤖 Generated with Claude Code
https://claude.ai/code/session_01TxSYmva6vWHCraKgSEe6Cu