Skip to content

Fix debug info nondeterminism in LowerStaticGlobalIntoAlloca - #8899

Open
Marijn Suijten (MarijnS95) wants to merge 1 commit into
microsoft:mainfrom
MarijnS95:upstream-determinism-fix
Open

Marijn Suijten (MarijnS95) wants to merge 1 commit into
microsoft:mainfrom
MarijnS95:upstream-determinism-fix

Conversation

@MarijnS95

@MarijnS95 Marijn Suijten (MarijnS95) commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

lowerStaticGlobalIntoAlloca() collects the instructions using a static global into a DenseMap<Instruction *, Value *> and then iterates it to rewrite each use onto the per-function alloca. That iteration order is pointer order, so it varies with heap addresses from run to run. The map has been a DenseMap since #3350, but nothing observable depended on the rewrite order until #7799 started creating a DILocalVariable per inlined DISubprogram for these localized globals: PatchDebugInfo() walks AI->users() to decide which subprograms are relevant and in which order, and rewriting the uses in pointer order appends the replacement instructions to that use list in pointer order. So the debug info comes out permuted.

The user-visible effect is that DXC emits a different ILDB part for byte-identical input, twice in the same process. We build a few thousand shader pipelines with -Zi -Qembed_debug and verify reproducibility by building every asset twice and comparing, and that check started failing on an arbitrary, run-dependent handful of them after we moved to v1.10.2605.37. Diffing the two containers part by part, ILDB is the only one that differs — DXIL, STAT, ISG1/OSG1/PSV0, ILDN and HASH are byte-identical — and putting both bitcode streams through llvm-bcanalyzer --dump yields dumps that are an exact permutation of one another: 92 DEBUG_LOC, 12 METADATA_LOCAL_VAR and 4 INST_CALL records with identical operands in a different order, plus the metadata renumbering that follows. Resolving the operands of the permuted LOCAL_VAR records gives names like global.kLowBound and global.kL2Children with DW_TAG_arg_variable and DISubprogram scopes, which is exactly what PatchDebugInfo() constructs. Codegen is unaffected, so this breaks reproducible builds and content-addressed caching of shader artifacts rather than the shaders themselves.

Switching to a MapVector rewrites the uses in the order they were collected, which is the module's use-list order. MapVector is already used elsewhere in this file. With a compiler built from this change, our check builds all 2503 pipelines clean three runs in a row, where before it failed every single run; I also confirmed the change does not perturb codegen by disassembling the DXIL part before and after, where the only differing record is the version string DXC stamps into the module.

On testing, I could use some guidance rather than guessing. CONTRIBUTING asks for a test that fails without the non-test change, and a plain FileCheck over the emitted order would be unreliable here, because the order this produced was a function of heap layout and a single run could pass either way. Two existing patterns look applicable: the double-compile-and-diff used by tools/clang/test/DXC/deterministic_output_resource_array.hlsl, or opt -run-twice as in tools/clang/test/DXC/Passes/reassociate/pointer-collision-non-determinism.ll, which exists for the same species of pointer-keyed-container bug and is the more deterministic of the two since the second run reallocates. I have not committed either, because I have no local build to validate that the test actually fails without the fix, and I would rather not add a test I have not seen fail. Happy to write whichever shape you prefer.

Worth noting there is close precedent in this same file: #5259 fixed debug info nondeterminism in SROAGlobalAndAllocas from stale staticGVs pointers, and its test comments already note that the scalarized variables are expected to appear in a deterministic order. PatchDebugInfo() is order-sensitive by construction, so anything that later feeds it an unordered container will reintroduce this.

Finally, we consume v1.10.2605.37 rather than main — would a backport to the corresponding release-preview-1.10.2605 branch be possible once this lands, so we can drop the commit we are carrying?

Copilot AI balanced review requested due to automatic review settings September 10, 2026 08:32
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

A regression test and release-note entry are required, and the new comment contains minor technical inaccuracies.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Replaces pointer-dependent iteration with insertion-ordered rewriting to make embedded debug information deterministic.

Changes:

  • Uses MapVector for deterministic global-use rewriting.
  • Documents why iteration order affects debug metadata.
File summaries
File Description
lib/Transforms/Scalar/ScalarReplAggregatesHLSL.cpp Preserves instruction collection order during static-global lowering.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 3
  • Review effort level: Balanced

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/Transforms/Scalar/ScalarReplAggregatesHLSL.cpp
Comment thread lib/Transforms/Scalar/ScalarReplAggregatesHLSL.cpp Outdated
Comment thread lib/Transforms/Scalar/ScalarReplAggregatesHLSL.cpp Outdated
@MarijnS95

Marijn Suijten (MarijnS95) commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

Squashed the branch to a single commit and rebased it onto current main, which also resolved a docs/ReleaseNotes.md conflict with the entry added by #8402. So the review threads above now show as outdated — the content they refer to is unchanged, only the history is flattened. Worth doing because the intermediate commit added the -run-twice test that turned out to be vacuous, and with squash-merge its retracted rationale would otherwise have ended up in the landed commit message.

Net diff is unchanged and still three files: the MapVector fix, the regression test, and the release note.

Marijn Suijten (MarijnS95) added a commit to MarijnS95/DirectXShaderCompiler that referenced this pull request Sep 10, 2026
Addresses review feedback on microsoft#8899.

The test re-runs `static-global-to-alloca` on a fresh clone of the module via
`opt -run-twice`, which allocates the instructions at different addresses and
reports a difference between the two outputs as an error, so it fails on the
unfixed pass without depending on which particular order that pass happened to
produce. Based on the existing `local_global_inline_scope.ll`, which already
covers this path with three users of the global and three inlined scopes; the RUN
line is repeated as in `pointer-collision-non-determinism.ll` to shrink the
chance of an ordering surviving both runs by coincidence.

Also corrects the new comment: a use list is prepended to rather than appended
to, and a DenseMap walks its buckets rather than ordering pointers.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 10, 2026 08:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The regression test’s -run-twice flow operates on an already-transformed clone and therefore does not verify the reported nondeterminism.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Balanced

Copilot AI review requested due to automatic review settings September 10, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The ordered-container fix is focused, documented, and covered by an appropriate regression test.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

lowerStaticGlobalIntoAlloca() collects the instructions using a static
global into a DenseMap keyed on Instruction*, then iterates it to rewrite
each use to the per-function alloca. A DenseMap walks its buckets, so that
order is pointer-derived and moves with heap addresses from run to run.

That used to be unobservable, but since microsoft#7799 added a DILocalVariable per
inlined DISubprogram for these localized globals, PatchDebugInfo() walks
AI->users() to decide which subprograms are relevant and in which order it
creates that debug info. Rewriting the uses in pointer-derived order mutates
AI's use list in that order, so the debug info comes out permuted.

The result is a compiler that emits a different ILDB part for
byte-identical input, twice in the same process: the same
METADATA_LOCAL_VAR and DEBUG_LOC records with identical operands in a
different order, and the metadata renumbering that follows. The DXIL part
itself is unaffected, so this only breaks reproducible builds and
content-addressed caching of shader artifacts, not codegen.

Use a MapVector so the uses are rewritten in the order they were collected,
which is the module's use-list order.

The regression test runs the pass in separate processes and compares the
outputs, as test/DXC/deterministic_output_resource_array.hlsl does, reusing
the module from local_global_inline_scope.ll which already covers this path
with three users of the global across three inlined scopes. That relies on
the runs laying out the heap differently, so it detects the regression with
high probability rather than with certainty; the test comment says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The implementation directly removes pointer-derived iteration order, with regression coverage and release-note documentation included.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Balanced

@llvm-beanz

Copy link
Copy Markdown
Collaborator

/azp run

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@llvm-beanz Chris B (llvm-beanz) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

In general for bugs like non-determinism I don't really look for a test case that exhibits the non-determinism (although that can be nice if possible), as much as much as verifying that we have tests that exercise the code and verify that it produces correct output.

DXC has a lot of gaps in test coverage, so adding a small bit of coverage for cases like this can add up over time to having better test coverage.

The HLSL SROA pass is one of my most hated parts of the DXC codebase because it is so undertested.

@MarijnS95

Copy link
Copy Markdown
Contributor Author

Chris B (@llvm-beanz) Yeah makes sense, I agree. Claude just found the requirement in docs/ReleaseNotes.md and Copilot restated it, so I let them add it 😬

Nielsbishere (Nielsbishere) pushed a commit to Oxsomi/DirectXShaderCompiler that referenced this pull request Sep 11, 2026
lowerStaticGlobalIntoAlloca() collects the instructions using a static global into a DenseMap keyed on Instruction*, then iterates it to rewrite each use to the per-function alloca. A DenseMap walks its buckets, so that order is pointer-derived and moves with heap addresses from run to run.

That used to be unobservable, but since microsoft#7799 added a DILocalVariable per inlined DISubprogram for these localized globals, PatchDebugInfo() walks AI->users() to decide which subprograms are relevant and in which order it creates that debug info. Rewriting the uses in pointer-derived order mutates AI's use list in that order, so the debug info comes out permuted.

The result is a compiler that emits a different ILDB part for byte-identical input, twice in the same process: the same METADATA_LOCAL_VAR and DEBUG_LOC records with identical operands in a different order, and the metadata renumbering that follows. The DXIL part itself is unaffected, so this only breaks reproducible builds and content-addressed caching of shader artifacts, not codegen.

Use a MapVector so the uses are rewritten in the order they were collected, which is the module's use-list order.

The regression test runs the pass in separate processes and compares the outputs, as test/DXC/deterministic_output_resource_array.hlsl does, reusing the module from local_global_inline_scope.ll which already covers this path with three users of the global across three inlined scopes. That relies on the runs laying out the heap differently, so it detects the regression with high probability rather than with certainty; the test comment says so.

Cherry-picked from MarijnS95:upstream-determinism-fix by Marijn Suijten (MarijnS95): microsoft#8899.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: New

Development

Successfully merging this pull request may close these issues.

3 participants