Repository navigation
Conversation
wychi
force-pushed
the
llir-source-mapping
branch
from
September 15, 2026 17:27
b8098c6 to
90bc327
Compare
Contributor
|
@wychi has imported this pull request. If you are a Meta employee, you can view this in D120178767. |
Summary: The LLIR stage is registered with `supports_source_mapping=True` and `parser_id="generic_loc"` on both the NVIDIA and AMD adapters, but `_parse_generic_loc` only understands MLIR `#loc` directives. LLVM IR records locations as `!dbg !N` references into `!DILocation` metadata, so the parser silently matches nothing and `source_mappings` ships without an `llir` key -- the stage is advertised as mapped and never is, with no warning. Add `extract_llvm_dbg_mappings()` / `_parse_llvm_dbg`, register the parser as `llvm_dbg` on the base adapter (it is LLVM, not vendor specific), and point both LLIR stage descriptors at it. `!DILocation` may carry `inlinedAt`, meaning the line belongs to an inlined callee in another file. `tl.sum` nests two frames deep (standard.py -> standard.py -> the user's kernel), so taking the innermost line naively attributes library code to the user's file. Following the existing callsite convention in `extract_loc_definitions`, `file`/`line` describe the callee and the call site is recorded separately; `inlined_at_file`/`inlined_at_line` give the root of the chain, which is the line in the user's kernel. Fixes #61. That issue lists the mappings that exist today -- Python<->TTIR, Python<->TTGIR, Python<->PTX, TTIR<->TTGIR, TTIR<->PTX, TTGIR<->PTX -- with LLIR absent from every pair, which is exactly what the broken `generic_loc` registration produces. It links `tritonparse/extract_source_mappings.py`; that module is now a CLI wrapper and the parsers have since moved to `parse/ir_parser.py`, where this change lands. The issue asks only for LLIR line mapping. Because the cross-stage join is already generic, wiring up the parser also yields LLIR<->TTIR, LLIR<->TTGIR and LLIR<->PTX/SASS (NVIDIA) or LLIR<->AMDGCN (ROCm), in both directions, at no extra cost. Both are measured below. Test Plan: Runs on CPU from a clean checkout; the example trace inlines `file_content` for every stage. (The checked-in `parsed_output_*` files are from an older tritonparse and are not a valid baseline -- re-parse the raw log on each branch.) python -m pytest tests/cpu -q TRITONPARSE_FB_MODE=0 python -m tritonparse.cli parse \ tests/example_output/logs --out /tmp/out --overwrite Tests: 720 passed, 1 skipped, 44 subtests (712 on main; +8). TestLLVMDebugMappings covers a plain !DILocation, a two-level inlinedAt chain, `!dbg` on a DISubprogram, and non-LLVM input. TestStageParserContract is the pair of guards above; replayed onto main both fail, naming the original bug. A/B on the example trace. main has no `llir` key and no entry in any stage carries `llir_lines`. This branch: kernel llir mapped callsites matmul_kernel/w1 149/293 6 matmul_kernel/w1 195/355 6 fused_op_kernel/w4 27/74 0 fused_op_kernel/w4 29/78 0 The existing cross-stage join then picks LLIR up in both directions with no further change -- for matmul_kernel/w1, entries gaining `llir_lines`: python 24/29, ttir 155/181, ttgir 160/186, ptx 174/216, sass 372/379. The callsite split matches the kernels: matmul_kernel calls `tl.cdiv`, which inlines; fused_op_kernel calls nothing that does. Blast radius: 2616 non-llir entries byte-identical to main. The only changes elsewhere are additive -- the `llir_lines` key, and one new python key (303, `tl.zeros`, which emits no PTX or SASS instruction but has explicit zero-init in LLVM IR). An audit of both branches for structural invariants and per-line TTIR construct agreement reports identical counts throughout: this adds a stage rather than altering the existing ones. AMD, on an 8x gfx950 host (torch 2.9.1+rocm6.4, pytorch-triton-rocm 3.4.0): `source_mappings` = [amdgcn, llir, python, ttgir, ttir], 63 llir entries, 44 callsites, 137 amdgcn entries gaining `llir_lines`. Other arches are checkable without hardware, since Triton's AMD backend compiles ahead of time for an explicit GPUTarget -- gfx90a, gfx942 and gfx950 each give 72 mapped lines and 42 callsites, with inlined frames resolving as on NVIDIA. Expected: the metadata comes from the shared pass lib/Target/LLVMIR/LLVMDIScope.cpp, which both backends invoke, and the parser never inspects the target triple. Not covered: gfx1100 (RDNA3), where the AMD backend segfaults during compilation regardless of this change. UI (Code Comparison, a pane set to LLIR, clicking python 287 `pid = tl.program_id(axis=0)`): | | screenshot | LLIR pane | | ------ | -------------------- | --------- | | before | https://pxl.cl/dfrv4 | nothing highlighted -- no llir mapping exists | | after | https://pxl.cl/dfrvc | L9 `%12 = tail call i32 @llvm.nvvm.read.ptx.sreg.ctaid.x()` | No frontend change needed: `CodeComparisonView` already reads `llir_lines` and `getViewableStages()` already admits the stage. Lint: `ruff check` and `ruff format --check` clean.
wychi
force-pushed
the
llir-source-mapping
branch
from
September 16, 2026 08:09
90bc327 to
fec2f37
Compare
FindHao
approved these changes
Sep 16, 2026
Contributor
meta-codesync Bot
pushed a commit
that referenced
this pull request
Sep 17, 2026
Summary:
Inlined Triton helpers — `tl.cdiv`, `tl.sum`, `tl.dot` — are mapped to the wrong source line. A mapping entry's `file`/`line` describe the **callee**, a line in a Triton library file, and `create_python_mapping` keys purely on `info["line"]`. So the arithmetic `tl.cdiv` generates is filed under `standard.py:43` and is unreachable from the kernel's source pane:
```
py288: num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)
filed under python 288: L106: #loc5 = loc("generate_examples.py":288:21) <- declarations only
L148: #loc56 = loc("num_pid_m"(#loc5))
filed under standard.py: L21: %num_pid_m = arith.addi %M, %c15_i32 <- the actual code
L22: %num_pid_m_2 = arith.divsi %num_pid_m, %c16_i32
```
Based on `main` and **independent of #434 — every path here is pre-existing and broken today. #434 added `inlined_at_file` / `inlined_at_line` for LLIR but left them unread; this makes them mean something for every stage. The two can land in either order.
## Three commits
**1 — `Map inlined code back to the line the user wrote`**
- **MLIR** `extract_loc_definitions`: a second pass walks each callsite's `callsite_caller` chain to the outermost frame and records `inlined_at_*`. Two passes are needed because during the first, a callsite's caller may not be in `locations` yet. The walk stops on `nxt is None` rather than a falsy check — `""` is the key of the bare `#loc`, so `loc(callsite(#locN at #loc))` is a real caller.
- **SASS** `_iter_sass_instructions`: nvdisasm emits one `//## File` comment per inline frame, innermost first, and the parser kept only the innermost — discarding the user's call site. It now tracks the last frame too, plus `is_callsite`, which SASS entries never carried.
- `create_python_mapping` prefers `inlined_at_line`, tested with `is not None` rather than `or`: a line of 0 is falsy and would silently re-file the entry under the callee.
- `inlined_at_*` is recorded only when the walk reaches a frame that is itself not a callsite. Breaking out on a missing, cyclic or unresolvable caller leaves the cursor on a callee — a library line — and writing that as the call site would be the very mis-attribution this pass removes. A broken chain leaves the entry plain, matching the PTX path when a `.file` index will not resolve.
**2 — `Fix three .loc defects on the inlined-code path`**
*Inlined `.loc` was never matched.* `PTX_LOC_PATTERN` required `//` right after the column, but inlined directives carry extra fields first:
```
.loc 2 43 13, function_name $L__info_string0, inlined_at 1 288 21 // standard.py:43:13
```
Instructions under them inherited whatever `.loc` came before, making PTX under-join on inlined code. On the example trace all 38 affected entries landed on the right python line *anyway*, because the preceding `.loc` happened to be the call site — luck, not correctness.
*`inlined_at` names the immediate caller, not the root.* Every other parser resolves to the outermost frame; taking the field verbatim does not. ptxas output for a two-level `tl.sum`:
```
.loc 2 273 12, ... inlined_at 2 313 12 standard.py -> standard.py
.loc 2 313 12, ... inlined_at 1 8 9 standard.py -> the kernel
```
The inner frame gets filed under `standard.py:313` — a library line — while MLIR, SASS and LLIR file the same instruction under the kernel line, and the stages silently disagree. The `.loc`'s own file index is now captured so a frame → caller map can be built, and the chain is walked to the root with a cycle guard. **Found in review by slp_generic**; the 17-kernel corpus audit missed it because `tl.cdiv` inlines one level deep, where the immediate caller and the root are the same line.
*`.loc <file> 0 <col>`* is DWARF for "no line information"; read literally it invented a mapping to python line 0, so the entry looked mapped and resolved to nothing. Now skipped, and it clears the current mapping so the next instruction doesn't inherit a stale one. This check sits after the `is_ptx`/`is_amdgcn` branch, so it **applies to AMDGCN too** — previously true but neither advertised nor tested.
`inlined_at_file` and `inlined_at_line` are written together or not at all. Setting the line while the `.file` index fails to resolve would let `create_python_mapping`, which keys on the line alone, file the entry under a caller line with nothing having checked the file it came from.
**3 — `Highlight the user's line when clicking inlined IR`** (`website/src`)
The reverse direction. `calculatePythonLines` reads the entry's own `file`/`line`, so `mapping.file.includes(pythonInfo.file_path)` is false for inlined code — the file is `language/standard.py`, not the kernel on screen — and the function returns `[]`. It now prefers `inlined_at_*`, requiring **both** fields. Plain entries carry neither and are untouched.
The Monaco rewrite moved this function from `CodeComparisonView.tsx` to `monaco/comparisonMapping.ts` and ported the defect with it, documenting the file match as "legacy direction preserved". It dropped the range check that used to reject the callee line a second time, so only one guard needs changing now.
Pull Request resolved: #435
Test Plan:
CPU only; the example trace inlines `file_content` for every stage.
```
python -m pytest tests/cpu -q
TRITONPARSE_FB_MODE=0 python -m tritonparse.cli parse tests/example_output/logs --out /tmp/out --overwrite
```
> The checked-in `tests/example_output/parsed_output_*` files are from an older tritonparse and are **not** a valid baseline — re-parse the raw log on each branch.
**Tests** — `main` 712 → +commit 1 `717` → +commit 2 `725`. Commit 3 is frontend-only: `npm run test:unit` 48/48, and reverting its fix fails exactly one case (47/48). `ruff check`/`format` clean per commit, no tabs in fixtures; `tsc`, `eslint`, `vite build` clean at the tip.
Commit 1 adds a broken-chain case; commit 2 adds cases covering the two-level chain resolving to the outermost frame, a cyclic chain terminating, an unresolvable `.file` index yielding no call site rather than a half-attributed one, the tab-separated form ptxas actually emits, and an AMDGCN line-0 directive.
**Commit 1**, `matmul_kernel`, python 288:
| | before | after |
|---|---|---|
| entries | ttir 2, ttgir 2, ptx 9, sass 4 | ttir 4, ttgir 4, ptx 9, **sass 8** |
| `is_callsite` | ttir 6, ttgir 6, sass 0 | ttir 6, ttgir 6, **sass 10** |
No `llir` column: that needs #434 present. What stays at `standard.py:43` is metadata, correctly — `//## File` comments and `#loc` definitions *declare* that location, and a declaration has no call site.
**Commit 2** — ptx entries with `line == 0` 18 → 0; ptx `is_callsite` 0 → 38; entries with a caller line but no caller file 0; python map containing key 0 in 2 compilations → 0.
| cross-stage join | before | after |
|---|---|---|
| sass → ptx | 931/967 | 967/967 |
| ttgir → ptx | 408/489 | 436/489 |
| ttir → ptx | 398/479 | 426/479 |
The ptx denominator drops by the 18 phantom line-0 entries.
The two-level chain fix is verified separately, by compiling a `tl.sum` kernel ahead of time for sm_90: every inlined PTX entry resolves to the single `tl.sum` line in the kernel rather than to `standard.py:313`. LLIR's `inlinedAt` chain on the same kernel confirms the expected root, `273 → 313 → kernel`.
**Corpus** — 17 kernels: the two from `tests/example_output/logs` plus 13 traced on an H100 covering elementwise, branchy, reduction, matmul and TMA shapes. Phantom line-0 entries: **143 on main, 0 here.**
**Commit 3** — replayed both resolver versions over every IR entry:
| | llir | ptx | sass | ttgir | ttir |
|---|---|---|---|---|---|
| entries | 400 | 588 | 967 | 489 | 479 |
| resolve before | 388 | 550 | 931 | 461 | 451 |
| resolve after | 400 | 588 | 951 | 473 | 463 |
| newly fixed | 12 | 38 | 20 | 12 | 12 |
| regressed / retargeted | 0 | 0 | 0 | 0 | 0 |
94 newly resolve, nothing regresses, and no entry that already resolved now points elsewhere — purely additive. (The `llir` column needs #434; the other four do not.)
## Screenshots
Clicking python 288 `tl.cdiv(M, BLOCK_SIZE_M)` — python → IR:
| | screenshot | |
|---|---|---|
| before | https://pxl.cl/dfqM4 | 5 highlights, every one a `#loc` declaration at the bottom of the IR; no instruction highlighted |
| after | https://pxl.cl/dfqMj | 9 highlights, adding the `arith.addi` / `arith.divsi` that `tl.cdiv` actually produced |
Clicking TTIR L21 `%num_pid_m = arith.addi %M, %c15_i32` — IR → python, the mirror case:
| | screenshot | |
|---|---|---|
| before | https://pxl.cl/dgnCL | ttir 21, ttgir 25/27 highlighted; python pane nothing, and it does not scroll |
| after | https://pxl.cl/dgnD8 | the same IR highlights, plus python 288 `num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)`, scrolled into view |
The `Loaded from:` banner identifies which build is shown. The second pair is on the Monaco UI at the commits in this stack; the first predates the rewrite, which does not change python → IR behaviour.
Reviewed By: FindHao
Differential Revision: D120381731
Pulled By: wychi
fbshipit-source-id: 43ceb380c4fbdae642f4906f0edc319bd219a246
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #61.
Summary
The LLIR stage is registered with
supports_source_mapping=Trueandparser_id="generic_loc"on both the NVIDIA and AMD adapters, but_parse_generic_loconly understands MLIR#locdirectives. LLVM IR records locations as!dbg !Nreferences into!DILocationmetadata, so the parser silently matches nothing andsource_mappingsships without anllirkey — the stage is advertised as mapped and never is, with no warning.This change adds
extract_llvm_dbg_mappings()/_parse_llvm_dbg, registers the parser asllvm_dbgon the base adapter (it is LLVM, not vendor specific), and points both LLIR stage descriptors at it.The
inlinedAtpart is the bit worth reviewing!DILocationmay carryinlinedAt, meaning the line belongs to an inlined callee in another file.tl.sumnests two frames deep:Taking the innermost line naively attributes library code to the user's file. Following the existing callsite convention in
extract_loc_definitions,file/linedescribe the callee and the call site is recorded separately;inlined_at_file/inlined_at_linegive the root of the chain, which is the line in the user's kernel.Scope
#61 lists the mappings that exist today — Python↔TTIR, Python↔TTGIR, Python↔PTX, TTIR↔TTGIR, TTIR↔PTX, TTGIR↔PTX — with LLIR absent from every pair, which is exactly what the broken
generic_locregistration produces. It linkstritonparse/extract_source_mappings.py; that module is now a CLI wrapper and the parsers have since moved toparse/ir_parser.py, where this change lands.The issue asks only for LLIR line mapping. Because the cross-stage join is already generic, wiring up the parser also yields LLIR↔TTIR, LLIR↔TTGIR and LLIR↔PTX/SASS (NVIDIA) or LLIR↔AMDGCN (ROCm), in both directions, at no extra cost. Both are measured below.
Test plan
Runs on CPU from a clean checkout; the example trace inlines
file_contentfor every stage.1. Tests
720 passed, 1 skipped, 44 subtests(712 on main; +8).TestLLVMDebugMappingscovers a plain!DILocation, a two-levelinlinedAtchain,!dbgpointing at aDISubprogramrather than a location, and non-LLVM input.TestStageParserContractadds two guards against the class of bug this PR fixes: a mappable stage must name a parser that can actually read its syntax, and the allowedsyntax_id → parser_idtable is exhaustive, so a stage whose syntax is absent fails rather than passes by default. Replayed onto main, both fail and name the original bug.2. A/B on the example trace
mainhas nollirkey, and no entry in any stage carriesllir_lines. This branch:matmul_kernelw1matmul_kernelw1fused_op_kernelw4fused_op_kernelw4The existing cross-stage join then picks LLIR up in both directions with no further change. For
matmul_kernelw1, entries gainingllir_lines:llir_linesThe callsite split matches the kernels:
matmul_kernelcallstl.cdiv, which inlines;fused_op_kernelcalls nothing that does.3. Blast radius
2616 non-
llirentries byte-identical to main. The only changes elsewhere are additive — thellir_lineskey, and one new python key (303,tl.zeros, which emits no PTX or SASS instruction but has explicit zero-init in LLVM IR). An audit of both branches for structural invariants and per-line TTIR construct agreement reports identical counts throughout: this adds a stage rather than altering the existing ones.4. AMD
On an 8× gfx950 host (
torch 2.9.1+rocm6.4,pytorch-triton-rocm 3.4.0), real launches and a real trace:source_mappings=[amdgcn, llir, python, ttgir, ttir]— 63 llir entries, 44 callsites, 137 amdgcn entries gainingllir_lines.Other arches are checkable without the corresponding hardware, since Triton's AMD backend compiles ahead of time for an explicit
GPUTarget:Inlined frames resolve as on NVIDIA. This is the expected result: the metadata comes from the shared pass
lib/Target/LLVMIR/LLVMDIScope.cpp, which both backends invoke, and the parser never inspects the target triple.Not covered: gfx1100 (RDNA3), where the AMD backend segfaults during compilation regardless of this change.
5. UI
Code Comparison, a pane set to LLIR, clicking python 287
pid = tl.program_id(axis=0):%12 = tail call i32 @llvm.nvvm.read.ptx.sreg.ctaid.x()No frontend change needed:
CodeComparisonViewalready readsllir_linesandgetViewableStages()already admits the stage.6. Lint
ruff checkandruff format --checkclean.