Semantics of llvm.loop.parallel_accesses and interaction with alloca

Hi all!

I’ve been trying to use the llvm.loop.parallel_accesses metadata lately, but I’m confused by its LangRef documentation not seemingly matching with the actual implementation. Its interactions with local variables in Clang also seem unpredictable and unintuitive to me.

The langref says the following about llvm.loop.parallel_accesses:

It denotes that no loop-carried memory dependence exist between it and other instructions in the loop with this metadata.

Yet for some reason, LoopAccessAnalysis.cpp still checks for stores involving loop-invariant addresses in such loops and LoopVectorizationLegality.cpp looks for those regardless of whether the loop is parallel or not. Which sounds odd, because the metadata is supposed to make the iterations independent, so those things shouldn’t matter, right?

Now, the part where the semantics get unpredictable is when an alloca is involved. Let’s take the following C code as an example:

void func(int *A, int i) {
    int t[32];
    // Could also be an unrollable loop, but unrolled manually to show that
    // this is not an outer-loop vectorization problem.
    t[0] = i; t[1] = i; t[2] = i; t[3] = i;
    t[4] = i; t[5] = i; t[6] = i; t[7] = i;
    t[8] = i; t[9] = i; t[10] = i; t[11] = i;
    t[12] = i; t[13] = i; t[14] = i; t[15] = i;
    t[16] = i; t[17] = i; t[18] = i; t[19] = i;
    t[20] = i; t[21] = i; t[22] = i; t[23] = i;
    t[24] = i; t[25] = i; t[26] = i; t[27] = i;
    t[28] = i; t[29] = i; t[30] = i; t[31] = i;
    A[i] = t[i];
}

void foo(int *A, int n) {
    #pragma clang loop vectorize(assume_safety)
    // or #pragma omp simd, both add `llvm.loop.parallel_accesses`.
    for (int i = 0; i < n; i++) {
        func(A, i);
    }
}

Now, to the C programmer, it looks like there are no dependencies between loop iterations, as the t array is fully contained inside a function which is called once per iteration. However, in Clang, func gets inlined and this emits an alloca inst in the first basic block of foo, outside of the loop; the same alloca is shared for the t of every iteration. As the end result, this loop is actually not safe to vectorize after all, and writes incorrect values to A: 7 7 7 7 7 7 7 7 15 15 15 15 15 15 15 15 23 23 23 23 23 23 23 23 31 31 31 31 31 31 31 31 (on my znver1 CPU). I opened an issue about this a bit earlier.

It seems difficult to use this metadata properly without the programmer constantly checking IR in between, as small things like using local arrays can suddenly make it unsafe. Ideally, I think the alloca should get privatized so that each vector lane would have its own copy of t instead of accidentally sharing it like here. I think that could work by adding the same llvm.access.group MD to alloca insts that is already used for loads and stores - this would carry enough information to be able to implement privatization in the loop vectorizer.

It feels like we’re in an unfortunate situation where llvm.loop.parallel_accesses doesn’t do exactly what it says on the tin (being a bit more conservative than it says), but also simultaneously interacts unsafely with the way high-level languages may emit alloca outside of the loop.

So in short, I have the following questions:

  1. Are loops with llvm.loop.parallel_accesses really supposed to care about stores to loop-invariant addresses or is this just an oversight?
  2. Is the above C program behaving as intended with llvm.loop.parallel_accesses?
  3. Is there any means to privatize allocas when vectorizing in LLVM? If not, would the project be open to such a thing being contributed?

For the record, since I proposed the parallel loop metadata (PLMD) originally to represent pure parallel loops originating from PoCL’s kernel compiler’s work-item loop generator:

Back then there were thorough discussions on how to avoid cases where passes not knowing the semantics of the PLMD would accidentally convert a parallel loop to a sequential one while it being still treated as a parallel loop by some other pass, and the idea was to mark all loads and stores with this MD and if non-marked stores appear, the loop is immediately downgraded to a sequential one.

Here the alloca is “hoisted” outside the loop, but since it’s not annotated with the MD, it is not included in the above rationale. Thus, the PLMD is wrongly attached to a serialized loop here (writes to a shared location, so it must be serial or it has a race?). Therefore, I proposed the solution to mark also the alloca and expand the LangRef to also cover the alloca with the semantics along the lines of “If the loop is parallelized, the allocas marked with the MD should be replicated per parallel loop iterations to retain correctness.”

Alloca sort of writes the memory as it touches the stack (pointer) so it’s not too far fetched given the original semantics. Actually the semantics actually should include also being able to ignore exception ordering as well given the original use case, but since OpenCL doesn’t mandate exceptions it is not there in the text (IIRC, or it just was forgotten). I believe omp simd also implies that serial exception ordering is not required.

1 Like

Adding loopvec maintainers for feedback. Do you think it would make sense to also allow marking allocas with the parallel metadata, for the reasons outlined by @pjaaskel above?
@fhahn @david-arm

Do you have an example that actually shows this issue? The example you shared has loads/stores from invariant addresses, and still gets vectorized.

Yes I think so.

AFAICT the issue is that the inliner moves the alloca to the function entry block, which invalidates the llvm.loop.parallel_accesses metadata. I think we either must not hoist the alloca or drop the llvm.loop.parallel_accesses, preferably the former

AFAICT the issue is that the inliner moves the alloca to the function entry block, which invalidates the llvm.loop.parallel_accesses metadata. I think we either must not hoist the alloca or drop the llvm.loop.parallel_accesses, preferably the former

To my understanding the problem here is that the inliner doesn’t invalidate the llvm.loop.parallel_accesses metadata implicitly. The inliner should not need to know about the MD semantics to ensure correctness but the MD’s semantics should “downgrade” the loop automatically to a serial one to guard against cases like these. Otherwise we’d have an MD of which semantics need to be known and preserved by the current and all future passes, which is naturally not the intention.

If the alloca is marked similarly with an MD that refers to the parallel loop by the inliner, when we encounter a store to an alloca which is not marked with the MD we could safely automatically fall back to a sequential loop by checking for it in isLoopParallel(). The other option of not marking it as a parallel loop at all if there’s an alloca inside might also throw useful cases out the window.

Luckily, I haven’t been able to find such a case yet. I was mostly wondering why the early-exit for IsAnnotatedParallel in analyzeLoop occurs after analyzing store instructions instead of before it, and saw that the analysis could technically have some effect in some circumstance I’m unaware of.

Taking @pjaaskel’s comment into account, I’m proposing that the alloca instructions whose memory is being written to by the loop, must be annotated with the corresponding llvm.access.group.

The idea is similar to what that MD does on load / store: passes that are unaware of the parallel semantics and do incompatible transformations will drop the metadata. Without that metadata in place, the loop is no longer considered parallel by isAnnotatedParallel().

This way, the problematic passes that hoist alloca, such as inlining, drop the metadata and trigger that fail-safe mechanism. If mem2reg later eliminates that alloca, then the loop can again be considered parallel.

This approach fixes the miscompile without requiring passes to be aware of the parallel metadata and allows existing users who privatize their allocas to simply keep doing that by attaching the llvm.access.group MD corresponding to the loop.

I’ve opened a PR that implements this approach: [LoopInfo] Don't recognize loop as parallel if it stores to out-of-loop alloca by juliusikkala · Pull Request #180551 · llvm/llvm-project · GitHub