The Loop That Runs Every XLA Pass: hlo_pass_pipeline.cc, Line by Line
A compiler pass is small and well behaved: it takes a program, makes one kind of improvement, and says whether it found anything. Running a few hundred of them back to back is a different job, and this is the 334-line C++ file that does it, read end to end. The two flags that skip passes, their four-shape grammar, and the two occurrence counters they resolve against. The invariant checkers between passes, why they only run after a pass that changed something, and the single rule that makes them safe. The debug mode that hashes the whole module twice to catch a pass that lied about its own return value. The dump condition with a special case hiding inside it, where the catch-all pattern writes fewer files than a narrow one. And the written ledger every pass appends a row to, opened before the skip decision so its numbering survives a bisect. The signature exhibit is the loop itself, ported line for line over a seven-pass pipeline, so you can watch a flag reshape a run.
Code walk · AI / ML. The source ↗
A free, interactive, animated visual explainer of The Loop That Runs Every XLA Pass: hlo_pass_pipeline.cc, Line by Line — built to be understood, not skimmed.
Questions
- How do I disable a single XLA pass?
- With --xla_disable_hlo_passes, which takes a comma-separated list of entries and is read at line 182 of hlo_pass_pipeline.cc. An entry has four possible shapes. A bare name such as algsimp matches every invocation of that pass, and also every pass inside a pipeline of that name, which is a deliberate extra rule in the matcher and the reason disabling something can kill far more than you expected. A scoped entry such as simplification/algsimp matches only invocations whose immediate parent pipeline is called simplification. A colon adds a zero-based occurrence, so algsimp:2 is the third invocation; scoped entries count occurrences inside the one pipeline instance, unscoped entries count them across the whole module. And @N matches a raw pass id and ignores every other field. The filters are parsed once before the loop starts, so a malformed entry fails the compile with an invalid-argument error rather than quietly matching nothing.
- Why does --xla_enable_hlo_passes_only still run other passes?
- Because the skip test carries an extra term. The condition at line 235 skips a pass when the filter does not match it AND the pass is not itself a pipeline, so nested pipelines are always entered and only leaf passes are filtered. The comment in the source states it directly: in disable mode, skip matching passes; in enable-only mode, non-matching leaf passes are skipped, pipelines are always entered. Without that carve-out, naming a single pass would match none of the pipelines wrapped around it, every one of them would be skipped, and the pass you asked for would never be reached. The other thing to know is that the two filter flags are mutually exclusive: setting both trips a hard CHECK at line 186 that aborts the process rather than picking one.
- What are XLA invariant checkers and when do they run?
- An invariant checker is a pass added with AddInvariantChecker that verifies the module without changing it; in practice it is the HLO verifier, which confirms operand shapes match, control-flow bodies have the right signatures, and nothing references an instruction from another computation. The pipeline runs every checker once before the first pass, with the marker name pipeline-start, and then after any pass that reported a change. A pass that changed nothing cannot have broken an invariant, which is what keeps the sweep affordable in a pipeline where most passes find nothing to do. When a check fails, the runner rebuilds the error message with the words Failed after and the name of the pass that just ran, so the failure names the pass responsible rather than the pass that later tripped over the damage.
- Why does --xla_dump_hlo_pass_re=.* write fewer files than a narrower pattern?
- Because of a special case in the dump condition at line 258. The in-loop dump is attempted when the pattern is non-empty AND either the pass reported a change or the pattern is not exactly .*, so the catch-all is read as a request for only the passes that did something, which stops a full-pipeline dump from writing hundreds of byte-identical modules. Any narrower pattern is attempted after every pass and filtered on the far side, inside the dump code, where three predicates decide whether a file is actually written: the module name must match the module pattern, the pipeline name must match the pipeline pattern, and either the pass before or the pass after must match the pass pattern. The one dump that ignores the special case is the pipeline-start dump at line 167, which is unconditional here.
- What does an XLA pass returning true or false actually control?
- More than it looks. The boolean is folded into a running total that the pipeline returns, and a fixed-point wrapper re-runs an inner pipeline until two consecutive rounds come back false, so a pass that reports true forever spins until a cap trips. It also gates the invariant checkers, gates the in-loop dump under the catch-all pattern, and is written into the module metadata row for that pass. Because so much hangs off it, the file has a debug mode that checks the claim by hashing the whole module before and after every pass: xla_unsupported_crash_on_hlo_pass_silent_hlo_change aborts when a pass reported no change while the hash moved, and xla_unsupported_crash_on_hlo_pass_noop_change aborts when a pass claimed a change that did not move the hash. Both are off by default because hashing an entire module per pass is expensive.
- Where does xla_run_hlo_passes_starting_from get handled?
- Not in the pipeline runner. It is read in HloPassInterface::Run, one level up in hlo_pass_interface.cc, which consults it before dispatching to any RunImpl. That has a visible consequence for anyone reading a dump. A pass skipped by xla_disable_hlo_passes still reaches the loop, still opens a metadata row at line 219, and still consumes a pass id, because the row is opened before the skip decision is made at line 234. A pass skipped by the starting-from flag never reaches this file at all, so it consumes nothing. The ordering is deliberate: it is what keeps the @N filter syntax and the ids in a dump filename referring to the same passes whether or not you were filtering when you produced them.