[LangRef] Clarify interaction of noalias and synchronization - #211507
Conversation
Noalias applies to accesses on other threads, but this was not very clear in existing wording.
|
@llvm/pr-subscribers-llvm-ir Author: Nikita Popov (nikic) ChangesNoalias applies to accesses on other threads, but this was not very clear in existing wording, because "during the execution of the function" is somewhat ambiguous. Explicitly mention that it applies to accesses from other threads. This means that conflicting accesses (i.e. not read-read) need to either happen-before function entry, or function exit needs to happen-before them, otherwise behavior is undefined. For accesses not based on the noalias pointer, this requires synchronization outside the function. Full diff: https://github.com/llvm/llvm-project/pull/211507.diff 1 Files Affected:
diff --git a/llvm/docs/LangRef.md b/llvm/docs/LangRef.md
index 415d015d39a93..c1d03be56aff4 100644
--- a/llvm/docs/LangRef.md
+++ b/llvm/docs/LangRef.md
@@ -1522,6 +1522,12 @@ Currently, only the following parameter attributes are defined:
met. For further details, please see the discussion of the NoAlias response
in {ref}`alias analysis <Must, May, or No>`.
+ `noalias` also applies to accesses from other threads, unless they
+ happen-before function entry, or function exit happens-before them. This
+ means that conflicting concurrent accesses from other threads either need
+ to be based on the noalias pointer, or else be appropriately synchronized
+ *outside* the function.
+
Note that this definition of `noalias` is intentionally similar
to the definition of `restrict` in C99 for function arguments.
|
|
cc @arsenm This clarification has direct relevance for GPU/SPMD code. HIP/OpenCL frontends lower restrict on kernel pointer parameters to noalias, including workgroup-shared buffers that are concurrently accessed by sibling threads and synchronized inside the kernel. Under the wording added here, those uses become UB, which affects existing and widespread GPU codegen patterns — so a note that this is the intended reading would be valuable. It also raises a design question: SPMD code typically wants the intra-thread guarantee (no other pointer in this thread aliases the parameter) but not the cross-thread no-concurrent-access guarantee — i.e. synchronizing operations should still be treated as clobbering. Is there appetite for a weaker attribute expressing exactly that? It would let such kernels stay correct without dropping aliasing precision entirely. Context and a motivating miscompile are in #211486. |
CUDA C++ does the same, but in CUDA C++ the illegal uses described by the new text are already undefined behavior and we aggressively optimize under that assumption today, so this change LGTM.
The current C __global__ void kernel(
int* restrict p,
int* restrict q
)
{
{
for ...
p[i] = q[i]
}
__syncthreads(); // acq_rel barrier arrive+wait
if (threadIdx.x == 0) q[i] = 42; // modify q[i] from other thread
__syncthreads();
{
for ...
p[i] = q[i]
}
}exhibits undefined behavior, because Instead what we'd want is to be able to assume that My preference would be to update C's restrict to just compose with concurrency and support the above; if C's restrict semantics were not updated when the C11 memory model was added, that may be an oversight (C++ does not have restrict, so the C++11 memory model change did not have to deal with this interaction). I don't know whether |
|
Thanks @gonzalobg — that example captures it exactly, and I agree with your analysis of why it's UB today. One framing I'd add for after this lands: once the cross-thread meaning of The encouraging part is that the semantics you describe — Concretely, running define i32 @two_shared_scoped(ptr %p1, ptr %p2, i32 %x) {
%a1 = load i32, ptr %p1, !alias.scope !0, !noalias !3
store i32 %x, ptr %p2, !alias.scope !3, !noalias !0
%a2 = load i32, ptr %p1, !alias.scope !0, !noalias !3 ; FORWARDED from %a1 -> p1 != p2 (intra-thread precision kept)
fence syncscope("workgroup") acq_rel
%a3 = load i32, ptr %p1, !alias.scope !0, !noalias !3 ; KEPT -> barrier clobbers (cross-thread ordering honored)
...
}The So my read: scoped-metadata lowering is a pragmatic frontend fix that makes these kernels correct today, with no new attribute and no LangRef change. A dedicated weaker attribute — or, as you'd prefer, |
|
@michaelselehov I love your idea of strengthening C's restrict at the parallel programming model level to support these cases and implementing that by lowering to alias scope metadata. |
|
I don't think we have any proposal for a precise semantics of alias scope metadata so I would caution about premature optimism in that regard.^^ I would expect that it has similar requirements wrt. accesses from other threads having to happen-before the beginning or happen-after the end of the scope. |
|
@RalfJung fair point, I don't want to rely on unspecified semantics either. Let me be precise about the reading we actually need, since it's narrower than what your caution is about, and it doesn't rely on in-function synchronization at all. All we want from lowering these The source guarantee is just "p and q are disjoint in this thread": __global__ void kernel(int* restrict p, int* restrict q) {
// p and q don't alias each other in this thread; nothing is claimed about other threads
...
}Two behaviors we need from it, on current IR ( ; (1) spatial disambiguation, thread-independent, no fence involved:
%a = load i32, ptr %p, !alias.scope !Sp, !noalias !Sq
store i32 %x, ptr %q, !alias.scope !Sq, !noalias !Sp ; can't touch p
%b = load i32, ptr %p, !alias.scope !Sp, !noalias !Sq ; forwarded to %a (p != q)
; (2) no cross-thread exclusivity: the metadata doesn't exempt synchronization:
%c = load i32, ptr %p, !alias.scope !Sp, !noalias !Sq
fence syncscope("workgroup") acq_rel
%d = load i32, ptr %p, !alias.scope !Sp, !noalias !Sq ; kept (the barrier clobbers p)(1) is the precision we want, and it holds regardless of other threads: a concurrent peer write to Ordering between producer and consumer is the barrier's job, a normal happens-before the program needs anyway, and it's separate from the aliasing claim. The aliasing claim never depends on it. That's why the pairwise scoped form is fine where the attribute isn't. I agree the exact semantics of alias-scope metadata isn't written down, and it'd be good to pin it down. But even the minimal "intra-thread pairwise disjointness" reading is enough to make these kernels correct, and it carries no other-thread happens-before obligation because it makes no other-thread claim. Happy to write this up as a concrete proposal if that helps. |
I don't know what this means. Let me try to make a guess: It seems like you just want to say "accesses through pointer p do not alias accesses done through pointer q", without making any statement about accesses done through pointers that are derived neither from p nor from q (such as the pointers that exist in other threads)? So, alias analysis should say "noalias" for p vs q, but should say "maybe alias" for p vs some other pointer r? However you also keep talking about threads as if they were somehow part of the definition of which aliasing is permitted, and I just can't understand what that could possibly mean here. I would expect that it doesn't matter where that other pointer r is used, this thread or other threads. So if I do then could that other function have used an alias r to clobber p or not? |
|
@RalfJung Yes, you've got it exactly, and your phrasing is better than mine: we want NoAlias(p, q) and MayAlias(p, r) for any r not derived from p or q, with nothing about threads in the definition. Drop the "in this thread" wording, that was just confusing. That narrow claim is all we need: it enables exactly the p/q disambiguation @gonzalobg and we want (reorder/forward p across a q access), and because it says nothing about any other pointer, it needs no happens-before. Ordering between producer and consumer stays the barrier's job, separate from aliasing.
Right, it could clobber p, so %d has to be reloaded. The scope metadata only says p doesn't alias q; it says nothing about that call, so the call is may-alias against p like any other unknown access. The only thing that could let %d forward is ordinary AA proving on its own that the call can't reach p (e.g. p is a non-escaping local), which has nothing to do with the p/q scopes. |
|
That definitely sounds like a reasonable thing to express. It is not what But anyway I think for this PR we just have to agree what |
…1507) Noalias applies to accesses on other threads, but this was not very clear in existing wording, because "during the execution of the function" is somewhat ambiguous. Explicitly mention that it applies to accesses from other threads. This means that conflicting accesses (i.e. not read-read) need to either happen-before function entry, or function exit needs to happen-before them, otherwise behavior is undefined. For accesses not based on the noalias pointer, this requires synchronization *outside* the function.
…1507) Noalias applies to accesses on other threads, but this was not very clear in existing wording, because "during the execution of the function" is somewhat ambiguous. Explicitly mention that it applies to accesses from other threads. This means that conflicting accesses (i.e. not read-read) need to either happen-before function entry, or function exit needs to happen-before them, otherwise behavior is undefined. For accesses not based on the noalias pointer, this requires synchronization *outside* the function.
…er (#10229) ## Motivation <!-- Explain the purpose of this PR and the goals it aims to achieve. --> Memory referenced via `__restrict__`-qualified pointers may not be modified in any way other than through said pointer so long as that pointer is alive. This is ambiguated in the context of multiple threads, but insofar as it is modeled by `noalias` in LLVM, accessing memory through a `__restrict__` pointer in one thread after having it modified by another thread violates this contract. JIRA ID: https://amd-hub.atlassian.net/browse/LCOMPILER-2487 ## Technical Details <!-- Explain the changes along with any relevant GitHub links. --> This is analogous to #9629. The semantics for LLVM's `noalias` between threads was clarified in llvm/llvm-project#211507 to also prohibit modifications through the same pointer from other threads. Unless `__restrict__` adopts a weaker guarantee in the future, `p_shared_block` is in violation of this contract. ## Test Plan <!-- Explain any relevant testing done to verify this PR. --> Build and run: `test_gemm_splitk` ## Test Result <!-- Briefly summarize test outcomes. --> Before: ``` [----------] Global test environment tear-down [==========] 32 tests from 8 test suites ran. (77718 ms total) [ PASSED ] 29 tests. [ FAILED ] 3 tests, listed below: [ FAILED ] TestGemmSplitK_MK_NK/0.SmallM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.MidLargeM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.Regular, where TypeParam = std::tuple<_Float16,_Float16,_Float16> 3 FAILED TESTS ``` With this patch, all tests pass. ## Submission Checklist - [x] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
fix(ck): [CK] LCOPMILER-2487: Remove erroneous `__restrict__` qualifier (#10229) ## Motivation <!-- Explain the purpose of this PR and the goals it aims to achieve. --> Memory referenced via `__restrict__`-qualified pointers may not be modified in any way other than through said pointer so long as that pointer is alive. This is ambiguated in the context of multiple threads, but insofar as it is modeled by `noalias` in LLVM, accessing memory through a `__restrict__` pointer in one thread after having it modified by another thread violates this contract. JIRA ID: https://amd-hub.atlassian.net/browse/LCOMPILER-2487 ## Technical Details <!-- Explain the changes along with any relevant GitHub links. --> This is analogous to #9629. The semantics for LLVM's `noalias` between threads was clarified in llvm/llvm-project#211507 to also prohibit modifications through the same pointer from other threads. Unless `__restrict__` adopts a weaker guarantee in the future, `p_shared_block` is in violation of this contract. ## Test Plan <!-- Explain any relevant testing done to verify this PR. --> Build and run: `test_gemm_splitk` ## Test Result <!-- Briefly summarize test outcomes. --> Before: ``` [----------] Global test environment tear-down [==========] 32 tests from 8 test suites ran. (77718 ms total) [ PASSED ] 29 tests. [ FAILED ] 3 tests, listed below: [ FAILED ] TestGemmSplitK_MK_NK/0.SmallM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.MidLargeM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.Regular, where TypeParam = std::tuple<_Float16,_Float16,_Float16> 3 FAILED TESTS ``` With this patch, all tests pass. ## Submission Checklist - [x] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
…mory (#10270) ## Motivation `segmented_blockreduce()` in `csrmm_device_nnz_split.h` takes two pointers into block-shared LDS, and both were marked `__restrict__`. Each thread reads `vals[tid - j]`, which another thread wrote. The attribute therefore does not hold. Clang lowers `__restrict__` to LLVM `noalias`. LangRef states that `noalias` also covers accesses from other threads, see llvm/llvm-project#211507. The compiler is free to forward the LDS reads across `__syncthreads()`. The reduction then drops the contributions of the neighbour threads and returns wrong values. The attribute also buys nothing here. The helper is force-inlined, and its two arguments are distinct `__shared__` arrays, so alias analysis already proves that they do not alias. JIRA ID : LCOMPILER-2518 ## Technical Details This is analogous to #10229 and #9629. The failure appears with a compiler that contains c3628c7f125b "Reapply [AA] No synchronization effects for never-escaping identified local". That commit tells alias analysis that a synchronizing operation cannot affect an object that never escapes the function. An LDS array declared inside a kernel is such an object. Together with the invalid `__restrict__`, this lets the compiler drop the cross-thread partial sums. I confirmed both sides on MI300X (gfx942): | compiler | source | result | | --- | --- | --- | | current weekly | `__restrict__` present | 40 pass, 8 fail | | current weekly | `__restrict__` removed | 48 pass | | same commit, AA change disabled | `__restrict__` present | 48 pass | ## Test Plan Build and run, on MI300X: ``` hipsparse-test --gtest_filter='*nightly/spmm_csc.generic*' ``` The failing cases use the Chebyshev4 matrix, `transA = T`, and `f64_r`. ## Test Result Before: ``` [==========] 48 tests from 1 test suite ran. [ PASSED ] 40 tests. [ FAILED ] 8 tests ``` With this patch, all 48 tests pass. ## Submission Checklist - [x] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests --------- Co-authored-by: Nico <31079890+ntrost57@users.noreply.github.com> Co-authored-by: Daniel So <dso@amd.com>
fix(ck): [CK] LCOPMILER-2487: Remove erroneous `__restrict__` qualifier ## Motivation <!-- Explain the purpose of this PR and the goals it aims to achieve. --> Memory referenced via `__restrict__`-qualified pointers may not be modified in any way other than through said pointer so long as that pointer is alive. This is ambiguated in the context of multiple threads, but insofar as it is modeled by `noalias` in LLVM, accessing memory through a `__restrict__` pointer in one thread after having it modified by another thread violates this contract. JIRA ID: https://amd-hub.atlassian.net/browse/LCOMPILER-2487 ## Technical Details <!-- Explain the changes along with any relevant GitHub links. --> This is analogous to #9629. The semantics for LLVM's `noalias` between threads was clarified in llvm/llvm-project#211507 to also prohibit modifications through the same pointer from other threads. Unless `__restrict__` adopts a weaker guarantee in the future, `p_shared_block` is in violation of this contract. ## Test Plan <!-- Explain any relevant testing done to verify this PR. --> Build and run: `test_gemm_splitk` ## Test Result <!-- Briefly summarize test outcomes. --> Before: ``` [----------] Global test environment tear-down [==========] 32 tests from 8 test suites ran. (77718 ms total) [ PASSED ] 29 tests. [ FAILED ] 3 tests, listed below: [ FAILED ] TestGemmSplitK_MK_NK/0.SmallM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.MidLargeM, where TypeParam = std::tuple<_Float16,_Float16,_Float16> [ FAILED ] TestGemmSplitK_MK_NK/0.Regular, where TypeParam = std::tuple<_Float16,_Float16,_Float16> 3 FAILED TESTS ``` With this patch, all tests pass. ## Submission Checklist - [x] Look over the contributing guidelines at https://github.com/ROCm/ROCm/blob/develop/CONTRIBUTING.md#pull-requests.
Noalias applies to accesses on other threads, but this was not very clear in existing wording, because "during the execution of the function" is somewhat ambiguous.
Explicitly mention that it applies to accesses from other threads. This means that conflicting accesses (i.e. not read-read) need to either happen-before function entry, or function exit needs to happen-before them, otherwise behavior is undefined. For accesses not based on the noalias pointer, this requires synchronization outside the function.