ROCm Composable Kernel #10574 — Why __restrict__ Was an Invalid Contract for Block-Shared LDS
ROCm CK LDS Aliasing, Part 1 of 2 — A workgroup-shared scratch pointer, LLVM noalias, and numerical values that became stale across a barrier
A barrier was already present.
The kernel still produced wrong results.
That combination can make the problem look like a hardware synchronization failure.
Threads write to LDS
↓
block_sync_lds()
↓
other threads read the LDS valuesIf the result is stale after that sequence, the first suspicion is often:
The barrier did not work.ROCm Composable Kernel PR #10574 found a different boundary.
The shared-memory pointer itself carried a contract that the program did not satisfy.
Several gridwise GEMM functions declared their block-shared scratch pointer as:
void* __restrict__ p_sharedBut p_shared referred to AMD’s workgroup-shared Local Data Share, or LDS.
The same physical memory was intentionally accessed by multiple threads.
Thread A
→ writes LDS
Thread B
→ reads the same LDS after synchronizationIn the affected compiler path, Clang lowered __restrict__ into LLVM’s noalias contract.
That contract allowed alias analysis to assume that conflicting accesses from other threads could not modify the object during the function invocation.
The source was therefore saying two different things at once.
Program structure:
Other threads write this shared object.Pointer contract:
Other accesses cannot conflict with this object
during the invocation.The barrier described the required execution order.
__restrict__ described an access relationship in which that cross-thread modification was not supposed to exist.
When LLVM became less conservative about synchronization effects on non-escaping local memory, the contradiction became observable.
On MI300X, the PR author reported:
Current staging compiler
+
__restrict__ present
→ FAILED
→ max error 130
→ 137,150 incorrect values
→ 22.78% of values wrongCurrent staging compiler
+
__restrict__ removed
→ PASSEDBaseline compiler with the relevant alias-analysis change reverted
+
__restrict__ present
→ PASSEDThe patch did not add another barrier.
It did not strengthen the existing barrier.
It removed the false aliasing promise from four p_shared parameters.
The synchronization operation was not missing.
The compiler had been given an invalid description of the memory being synchronized.
What p_shared represented
The affected functions were gridwise GEMM implementations inside Composable Kernel.
Their signatures included ordinary global-memory operands such as:
A tensor pointer
B tensor pointer
output tensor pointerand one scratch pointer:
void* p_sharedThat scratch pointer referred to LDS allocated for the workgroup.
On AMD GPUs, LDS is memory shared by threads in the same workgroup.
It is commonly used to stage:
input tiles,
transformed values,
partial results,
layout-converted fragments,
and intermediate data reused by neighboring threads.
The broad pattern is:
Global-memory tile
↓
threads cooperatively write LDS
↓
workgroup barrier
↓
threads cooperatively read LDS
↓
matrix instructions or further transformationsThe memory is useful precisely because its ownership is collective.
It is not private storage associated with one thread.
The pointer value may be identical in every thread
A simplified kernel can be imagined like this:
__device__ void run(void* p_shared) {
auto* scratch = static_cast<float*>(p_shared);
if (thread_id == 0) {
scratch[0] = 42.0f;
}
block_sync_lds();
if (thread_id == 1) {
consume(scratch[0]);
}
}The intended result is:
Thread 0 writes 42
barrier completes
Thread 1 reads 42Every participating thread can receive the same LDS base address.
Thread 0 p_shared
→ address X
Thread 1 p_shared
→ address X
Thread 2 p_shared
→ address XThat is not an accidental alias.
It is the design.
The pointer names the workgroup’s shared scratch region.
__restrict__ is not merely a performance suggestion
It is tempting to read __restrict__ as:
Please optimize this pointer aggressively.Its meaning is stronger.
It gives the compiler information about how the pointed-to memory may be accessed.
In an ordinary single-threaded example:
void update(
float* __restrict__ dst,
const float* __restrict__ src) {
...
}the qualifier tells the compiler that accesses based on dst do not conflict with accesses through another unrelated pointer such as src.
That promise can enable:
load reuse,
store motion,
vectorization,
dead-load removal,
and stronger alias-analysis conclusions.
The compiler is allowed to transform the program according to that promise.
If the promise is false, the transformed program is not required to preserve the behavior the programmer expected.
The contract also matters across threads
The important detail in #10574 was not only that several pointer expressions could reach the same address inside one thread.
LLVM’s noalias contract also applies to conflicting accesses from other threads.
The clarified LLVM rule states that conflicting accesses not based on the noalias pointer must either:
happen before the function invocation beginsor:
happen after that invocation has finishedAn access performed concurrently by another thread during the invocation does not satisfy that boundary merely because the two threads meet at a barrier inside the function.
For a workgroup-shared object:
Thread A function invocation
→ writes p_shared
Thread B function invocation
→ reads or writes the same object
both invocations overlapThe cross-thread access is part of the kernel’s normal execution.
That is incompatible with treating each invocation’s pointer as an exclusive noalias access root.
The source contained a false ownership claim
The affected declaration looked approximately like this:
__device__ static void Run(
const ADataType* __restrict__ p_a_grid,
const BDataType* __restrict__ p_b_grid,
EDataType* __restrict__ p_e_grid,
void* __restrict__ p_shared,
...
);The global tensor pointers and the LDS pointer do not necessarily have the same ownership contract.
Global input and output pointers
A kernel can legitimately require that:
A
B
outputdo not overlap in prohibited ways.
The caller can provide separate allocations.
The __restrict__ declaration may describe a valid kernel precondition for those arguments.
LDS scratch pointer
p_shared is intentionally shared among threads.
Many thread invocations
→ same scratch object
→ conflicting writes and reads
→ synchronization inside the kernelThe same qualifier therefore does not automatically make sense for this parameter.
The problem was not that __restrict__ is always unsafe in GPU code.
The problem was that it was attached to a pointer whose purpose contradicted its exclusivity contract.
A barrier orders accesses that the compiler still believes can exist
A workgroup barrier normally establishes an execution boundary such as:
writes before barrier
happen before
reads after barrierBut that description assumes the compiler recognizes that another thread may modify the relevant memory.
Now add the false noalias fact.
From one thread’s optimized view:
I have an exclusive access contract for this object.
Other threads cannot conflict with it during my invocation.The barrier may still synchronize control flow.
But alias analysis can conclude that the shared object itself was not modified by any access the current invocation must consider.
A previously loaded LDS value may therefore appear reusable.
load LDS value
barrier
load same address againcan be reasoned about as though it were:
load LDS value once
barrier
reuse register valueif the compiler believes no legal external write could have changed the object.
The hardware barrier cannot force a reload that the compiler removed before machine code was emitted.
A conceptual stale-value example
The following is a minimal conceptual model, not the exact CK reproducer:
__device__ void example(void* __restrict__ p_shared) {
float* shared = static_cast<float*>(p_shared);
float before = shared[0];
if (thread_id == 0) {
shared[0] = 42.0f;
}
block_sync_lds();
float after = shared[0];
consume(after);
}The programmer expects:
before
→ old value
after
→ value written by Thread 0But if the compiler accepts the noalias contract as true:
No conflicting other-thread modification is legal
during this invocation.then it may reason:
shared[0] cannot have changed
through a relevant external access
therefore
after == beforeThe exact instruction sequence removed or retained in the failing CK kernel was not published in the PR.
The important source-level contradiction is visible without that final disassembly:
cross-thread write is required by the algorithm
while
cross-thread modification is excluded by the pointer contractWhy the failure appeared after an alias-analysis change
The source qualifier existed before the reported failure.
That does not mean it was correct before.
An invalid optimization contract can remain latent while the compiler is conservative.
False source assumption
+
optimizer does not exploit it
→ output happens to remain correctA later compiler update can begin using the information more aggressively.
Same false source assumption
+
stronger alias analysis
→ transformation becomes legal under the declared contract
→ wrong result appearsThe PR specifically linked the failure to an LLVM alias-analysis change titled:
No synchronization effects
for never-escaping identified localLLVM traditionally models fences and stronger atomic operations as reading and writing broad memory regions so that optimizations respect synchronization.
The referenced change stopped applying those broad effects to certain identified local objects that do not escape.
That can be a valid optimization.
A genuinely private local object cannot be modified by another thread merely because a synchronization instruction exists.
The affected CK pointer, however, was not genuinely private in the way its noalias contract implied.
Optimizer improvement
+
incorrect source-level alias fact
→ latent defect becomes observableThe compiler update did not invent the sharing
The shared access already existed in the algorithm.
Thread group cooperatively uses LDSRemoving an overly conservative memory effect did not create that behavior.
It exposed a mismatch that had already been present.
This distinction matters.
Incorrect explanation
The newer LLVM compiler broke LDS synchronization.More precise explanation
The kernel declared a block-shared LDS pointer as restrict.
A newer alias-analysis rule relied more strongly on that declaration.
The resulting optimization no longer preserved the behavior
the source author expected from cross-thread sharing.The patch repaired the source contract rather than reverting the compiler optimization.
The before-and-after matrix isolated both sides
The PR author tested three combinations on MI300X.
| Compiler | Source qualifier | Result |
|---|---|---|
| Current staging | __restrict__ present | Failed |
| Current staging | __restrict__ removed | Passed |
| Baseline with AA change reverted | __restrict__ present | Passed |
This matrix is useful because each comparison answers a different question.
Same compiler, different source
Current staging + restrict
→ fail
Current staging - restrict
→ passThis isolates the source qualifier.
Same source, different alias-analysis behavior
Current staging + restrict
→ fail
baseline/reverted AA + restrict
→ passThis identifies the compiler change that made the invalid assumption operationally visible.
Together:
Invalid source promise
×
optimizer willing to exploit it
→ numerical failureThe failure was numerical, not merely theoretical
The published reproducer was:
TestGroupedConvndBwdData2d/9.Test2DThe affected implementation was a grouped-convolution backward-data kernel built through gridwise GEMM code.
Before the patch, the reported output included:
max error:
130
number of errors:
137,150
incorrect values:
22.78%After removing __restrict__:
1 test passedThe test did not merely check compilation.
It performed a numerical comparison and observed incorrect results before the patch.
That moves the finding beyond static analysis.
Invalid alias contract
→ identified in source
Wrong numerical result
→ reproduced on MI300X
Minimal source repair
→ test passesThe patch changed four declarations
The landed diff removed __restrict__ from p_shared in four places.
Three were in:
gridwise_gemm_multiple_d_xdl_cshuffle.hppOne was in:
gridwise_gemm_xdlops_v2r3.hppThe change was structurally small:
- void* __restrict__ p_shared
+ void* p_sharedThe patch did not change:
workgroup dimensions,
tile dimensions,
synchronization placement,
LDS allocation size,
matrix-instruction selection,
input layouts,
output layouts,
or numerical tolerances.
It removed one compiler promise that was not true for the object.
A one-word qualifier can change generated code
At the C++ surface, this looks like a small declaration edit.
remove __restrict__At the compiler boundary, it changes the alias relation available to optimization passes.
Before
p_shared
→ exclusive noalias access rootAfter
p_shared
→ may be affected by accesses not derived
from this thread invocation’s pointerThis can force the compiler to preserve loads or memory dependencies that were previously treated as redundant.
No new runtime instruction needs to be written manually in the source.
Changing the semantic contract changes which machine-code transformations are legal.
More optimization information is useful only when it is true
__restrict__ can improve performance when it describes the program accurately.
For example:
A allocation
→ read only
B allocation
→ read only
C allocation
→ distinct outputTelling the compiler these regions do not overlap can be valuable.
The same reasoning does not justify placing the qualifier on every pointer in a function signature.
A pointer should be marked __restrict__ only when the program can satisfy the corresponding access contract.
The correct question is not:
Would alias analysis become stronger?It is:
Is the stronger alias statement true for every legal execution?For block-shared LDS with cross-thread writes and reads, the answer was no.
The physical address alone does not define the violation
It would be too simplistic to say:
Two threads have the same pointer value,
therefore restrict is always invalid.Read-only access from several threads does not create the same conflicting-write problem.
The relevant combination is:
overlapping thread invocations
+
same logical memory object
+
at least one conflicting write
+
accesses expected to communicate through that objectThe CK scratch buffer met that condition.
Threads produce values in LDS
other threads consume those values
barrier orders the communicationThe object was a communication channel between threads, not merely a shared immutable lookup table.
A workgroup barrier is evidence of shared ownership
The presence of:
block_sync_lds()near the scratch-memory path is itself a strong architectural clue.
Private memory does not require a workgroup-wide barrier.
The barrier exists because:
one subset of threads produces state
another subset may consume itThat is exactly the condition a reviewer should compare against a __restrict__ declaration.
Pointer marked exclusive
+
workgroup barrier protecting the same object
→ ownership contract deserves inspectionThe barrier does not automatically prove the qualifier is wrong.
But it tells the reviewer that memory ownership is not purely local to one thread.
Shared-memory APIs need to express collective ownership
A raw pointer type does not reveal whether the address refers to:
thread-private memory
workgroup-shared memory
device-global memory
host-visible memoryThe semantic meaning exists in:
allocation origin,
address space,
calling convention,
surrounding synchronization,
and documentation.
When an API exposes all of those through:
void*qualifiers and comments become especially important.
A misleading qualifier can be more dangerous than an absent one because it supplies the compiler with a false proof.
The patch did not add a stronger synchronization primitive
This is important because the visible symptom involved stale shared data.
A common repair might have been:
add another barrier
add a fence
make access volatile
use stronger atomic operationsPR #10574 did none of those.
That is consistent with the root cause.
Actual algorithmic ordering
→ already present
Compiler model of the memory
→ incorrectAdding synchronization without correcting the alias contract could leave the compiler free to continue treating the object as unaffected by legal external writes.
The source first needed to stop promising exclusive access.
volatile would have answered a different question
Marking accesses volatile can force individual loads and stores to remain observable in particular ways.
It does not repair the meaning of an invalid noalias promise.
volatile
→ concerns whether accesses may be removed or reordered
under volatile semanticsrestrict / noalias
→ concerns which accesses may refer to the same objectUsing volatility to compensate for false alias information would mix two contracts.
The landed fix changed the one that was wrong.
An extra barrier would also have answered a different question
Another barrier could establish another execution rendezvous.
But the kernel already had a barrier separating producers and consumers.
producer writes
↓
existing block_sync_lds
↓
consumer readsThe mismatch was not:
consumer can physically run before producerIt was:
compiler can conclude that the producer’s cross-thread write
is not a legal modification of this noalias objectMore rendezvous points do not make the false exclusivity statement true.
The patch scope was narrow
The change should not be generalized into:
Composable Kernel cannot use __restrict__.or:
LLVM noalias is unsafe for GPU code.The confirmed scope is:
four p_shared parameters
in two gridwise GEMM headers
used by a failing grouped-convolution backward-data path
under the reported staging compiler
on MI300X / gfx942Other pointers may satisfy the qualifier.
Other kernels may use LDS through different signatures.
Other compiler versions may not expose the same wrong result.
The patch repairs the declarations that the reproduced path demonstrated were invalid.
The failure was not established as an MI300X hardware defect
The hardware executed the generated program.
The public evidence points to:
source alias contract
×
compiler optimizationnot an LDS coherency defect in the device.
The same hardware passed when:
the qualifier was removedand the same source passed when:
the relevant compiler alias-analysis behavior was revertedThat comparison is inconsistent with a simple hardware barrier failure.
The exact stale instruction was not published
The PR describes the compiler as being able to retain an LDS value in a register across the barrier.
Its before-and-after matrix strongly supports the alias-analysis mechanism.
But the public PR does not include:
a complete pre-patch LLVM IR dump,
a complete post-patch LLVM IR dump,
the exact load eliminated by alias analysis,
or a machine-code diff identifying one reused register.
The article should therefore distinguish:
Directly reproduced:
wrong numerical output with restrict
correct output without restrict
correct output with older/reverted AA behaviorfrom:
Mechanistic explanation reported by the PR:
invalid noalias allowed LDS state to remain stale
across synchronizationThe source-level contract and validation matrix are strong.
The precise final instruction transformation was not separately published.
The patch reused an existing numerical test
The PR did not add a new dedicated regression-test file.
It used the existing:
TestGroupedConvndBwdData2d/9.Test2Dto show:
before
→ FAIL
after
→ PASSThat is meaningful runtime evidence.
It leaves a maintenance question.
A future change could reintroduce __restrict__ or an equivalent alias contract without a newly added test explicitly documenting:
This test protects cross-thread LDS visibility
under current alias analysis.The existing numerical test may continue to protect the path indirectly.
The patch does not add a minimal compiler-specific regression asserting the alias or memory-dependency boundary itself.
A focused regression could pin the contract directly
A small GPU test could construct a workgroup-shared buffer where:
one lane writes
all lanes synchronize
another lane reads
the read value participates in the outputThe test would then compile under the optimization configuration that exposed the CK failure.
Its purpose would not be to test the hardware barrier in isolation.
It would test:
shared pointer contract
+
cross-thread modification
+
compiler alias analysis
+
barrier visibilityA compiler-level test could separately inspect whether a second LDS load is retained when external thread modification is possible.
These are proposed test directions, not tests added by #10574.
What the patch directly confirms
The merged PR confirms that:
p_sharedwas declared with__restrict__in four gridwise GEMM function parameters,those parameters referred to block-shared LDS used across threads,
the qualifier was removed from all four sites,
the reported grouped-convolution backward-data test failed numerically with the qualifier,
the same test passed after the qualifier was removed,
the qualifier-bearing source passed when the relevant LLVM alias-analysis change was reverted,
the reproduced environment was MI300X / gfx942,
and the patch was merged into
rocm-librariesdevelop.
What the patch does not confirm
The public evidence does not establish that:
every CK kernel using LDS had the same defect,
every use of
__restrict__in CK is invalid,all AMD GPU architectures reproduce the same numerical failure,
every LLVM version containing the referenced AA change exposes it,
the exact eliminated LDS load is documented in public IR,
the failure affects every grouped-convolution shape,
the patch changes kernel performance,
or a new minimal regression test was added specifically for the alias contract.
The supported conclusion is narrower:
Four block-shared LDS pointer parameters carried an exclusivity contract that the reproduced kernel did not satisfy, and removing that contract restored numerical correctness under the reported compiler.
The source bug and the compiler trigger were both necessary
The failure can be expressed as two conditions.
Condition 1 — invalid source fact
p_shared is workgroup-shared
but
p_shared is declared restrictCondition 2 — optimizer uses the fact
synchronization no longer forces broad memory effects
for an object considered local and non-escapingTogether:
cross-thread LDS update
→ excluded from the compiler’s legal memory model
→ old value can survive optimization
→ numerical output becomes wrongRemoving either condition hides the failure.
Remove restrict
→ compiler must account for aliasing
→ PASSUse more conservative AA
→ false promise not fully exploited
→ PASSOnly the first repair corrects the source contract.
The deeper boundary is ownership, not syntax
The code change was one word.
The bug was not grammatical.
void* __restrict__ p_sharedcompiled correctly.
The compiler accepted the declaration exactly as written.
The mismatch was between:
declared ownershipand:
actual ownershipThe source described the object as exclusive.
The algorithm used it collectively.
Shared state must remain visible to the compiler as shared state
A GPU barrier is meaningful only when the compiler preserves the memory communication it orders.
Producer thread writes state
↓
barrier establishes ordering
↓
consumer thread reloads stateIf a pointer contract tells the compiler that the producer’s write cannot legally affect the consumer’s object, the dependency graph is broken before the barrier reaches hardware.
Synchronization orders real dependencies.
It cannot recover a dependency that an invalid alias contract told the compiler did not exist.
For block-shared LDS, the pointer’s type and qualifiers must leave room for the cross-thread writes that make the scratch buffer useful.
That was the contract repaired by ROCm Composable Kernel #10574.
Part 2: why the barrier could not repair noalias
The second and final article examines the compiler side in more detail.
C++ __restrict__
→ LLVM noalias
→ other-thread access semantics
→ synchronization effects in alias analysis
→ non-escaping local-memory reasoning
→ stale value retained across block_sync_lds()It will distinguish:
hardware execution orderingfrom:
compiler memory-dependence modelingand explain why LLVM’s optimization was valid under the declared noalias contract even though the program behavior expected by CK was not.
Related material
ROCm Composable Kernel PR #10574 — Remove erroneous
__restrict__qualifiergridwise_gemm_multiple_d_xdl_cshuffle.hppat the merged commitLLVM #211507 — Clarify interaction of
noaliasand synchronizationLLVM #196923 — No synchronization effects for never-escaping identified local
Patch status: Merged into ROCm rocm-libraries develop
Affected component: Composable Kernel gridwise GEMM
Affected pointer: Workgroup-shared LDS scratch pointer p_shared
Source change: Removed four __restrict__ qualifiers across two headers
Observed failure: Numerical mismatch in TestGroupedConvndBwdData2d/9.Test2D
Reported hardware: MI300X, gfx942
Pre-fix result: Max error 130, 137,150 errors, 22.78% incorrect values
Post-fix result: Test passed
Compiler control: Qualifier-bearing source passed with the relevant LLVM AA change reverted
Direct patch scope: Alias contract only; no new barrier, tile, layout, or kernel algorithm
Regression evidence: Existing numerical test used; no dedicated new test file added
Evidence boundary: Exact final load/register transformation was not published as a complete IR or ISA diff
This is Part 1 of a two-part series on ROCm Composable Kernel’s block-shared LDS aliasing boundary.
This article examined why p_shared could not truthfully carry an exclusive __restrict__ contract when the GEMM algorithm intentionally communicated through that LDS object across threads.
Part 2 examines how that source qualifier became LLVM noalias, why noalias constrains accesses from other threads, and why an in-function workgroup barrier could not restore a dependency the compiler had already been told was impossible.
#ROCm #ComposableKernel #AMD #MI300X #LDS #GPUProgramming #Restrict #NoAlias #AliasAnalysis #CompilerOptimization #CodeAnalysis