microsoft / microsoft/DirectXShaderCompiler

DXC emits racey zero-init code when numthreads(1,1,1)

Open
#6,352 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug correctness
Dominant language
C++
Stars
3.7k
Forks
900
Avg merge
2d 11h
Merged PRs (30d)
44

Description

Description
With a compute shader with numthreads(1,1,1) where a groupshared variable is initialized to zero, DXC emits different code depending on whether the write is conditional on the group index. One of these results in race conditions on some GPUs.

Steps to Reproduce

See this compiler explorer example: https://godbolt.org/z/GPjP58To9

The interesting bit is the function that initializes the groupshared var to 0:

void tint_zero_workgroup_memory(uint local_idx) {
#ifdef CHECK_LOCAL_INDEX
    if (local_idx < 1)
#endif
    {
        testVar = (0.0f).xxx;
    }
    GroupMemoryBarrierWithGroupSync();
}

There are 2 compilations, one without CHECK_LOCAL_INDEX defined, and one where it is defined. The diff is also shown at the bottom. The main things to note between the two:

  • When not checking the index, DXC emits the variable with the zeroinitializer tag, and there is no explicit code to zero-init the variable before the call to @dx.op.barrier.

  • Whereas with the index check, it emits the same variable with the undef tag, and there is now explicit code to zero-init the variable:

  store float 0.000000e+00, float addrspace(3)* getelementptr inbounds ([3 x float], [3 x float] addrspace(3)* @"\01?testVar@@3V?$vector@M$02@@A.v", i32 0, i32 0), align 4, !dbg !92
  store float 0.000000e+00, float addrspace(3)* getelementptr inbounds ([3 x float], [3 x float] addrspace(3)* @"\01?testVar@@3V?$vector@M$02@@A.v", i32 0, i32 1), align 4, !dbg !92
  store float 0.000000e+00, float addrspace(3)* getelementptr inbounds ([3 x float], [3 x float] addrspace(3)* @"\01?testVar@@3V?$vector@M$02@@A.v", i32 0, i32 2), align 4, !dbg !92
  br label %"\01?tint_zero_workgroup_memory@@YAXI@Z.exit.i", !dbg !94

"\01?tint_zero_workgroup_memory@@YAXI@Z.exit.i":
  call void @dx.op.barrier(i32 80, i32 9), !dbg !95

Our compiler (Tint) emits HLSL without a group index check when numthreads(1,1,1), since we reason that at most one thread is executing the code. In practice, we have seen this fail on some GPUs intermittently - in other words, the zero-init is racey. We can fix this, of course, by simply adding the conditional check for the index; but we'd like to know:

  1. Is this is actually a bug in DXC?
  2. Or is the problem that when writing HLSL, we cannot assume that the compiler uses numthreads to reason about data races, and must always guard access to shared variables?
  3. What does the zeroinitializer tag actually mean? Are GPUs supposed to ensure the memory is zero-initialized before any workgroup threads begin execution? Are certain GPU implementations wrong if they're not handling this properly?

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the compiler explorer example linked in the issue and compare the two generated DXC outputs for the CHECK_LOCAL_INDEX variants. Investigate the zeroinitializer and undef handling around the groupshared stores and dx.op.barrier. Done means establishing whether DXC or the HLSL assumptions are incorrect and identifying the required compiler or test change.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.