Skip to content

[LLD] Avoid non-deterministic relocations processing. #107186

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed

Conversation

avl-llvm
Copy link
Collaborator

@avl-llvm avl-llvm commented Sep 4, 2024

TaskGroup has a "sequential" mode for deterministic processing. LLD uses this for relocations handling. But, As it places relocations to different vectors(based on getThreadIndex), the actual locations can be non-deterministic:

if (shard)
  part.relrDyn->relocsVec[parallel::getThreadIndex()].push_back(
      {&isec, isec.relocs().size() - 1});

It is neccessary to use the same thread index to have the same locations. This patch changes "sequential" mode to always execute tasks on single thread with index 0.

Fixes this #105958.

@llvmbot
Copy link
Member

llvmbot commented Sep 4, 2024

@llvm/pr-subscribers-lld-elf
@llvm/pr-subscribers-lld

@llvm/pr-subscribers-llvm-support

Author: Alexey Lapshin (avl-llvm)

Changes

TaskGroup has a "sequential" mode for deterministic processing. LLD uses this for relocations handling. But, As it places relocations to different vectors(based on getThreadIndex), the actual locations can be non-deterministic:

if (shard)
  part.relrDyn->relocsVec[parallel::getThreadIndex()].push_back(
      {&isec, isec.relocs().size() - 1});

It is neccessary to use the same thread index to have the same locations. This patch changes "sequential" mode to always execute tasks on single thread with index 0.

Fixes this #105958.


Full diff: https://github.com/llvm/llvm-project/pull/107186.diff

3 Files Affected:

  • (modified) llvm/include/llvm/Support/Parallel.h (+2-3)
  • (modified) llvm/lib/Support/Parallel.cpp (+2-11)
  • (modified) llvm/unittests/Support/ParallelTest.cpp (+6-1)
diff --git a/llvm/include/llvm/Support/Parallel.h b/llvm/include/llvm/Support/Parallel.h
index 8170da98f15a8c..0e1f4fadcf4fe6 100644
--- a/llvm/include/llvm/Support/Parallel.h
+++ b/llvm/include/llvm/Support/Parallel.h
@@ -96,9 +96,8 @@ class TaskGroup {
 
   // Spawn a task, but does not wait for it to finish.
   // Tasks marked with \p Sequential will be executed
-  // exactly in the order which they were spawned.
-  // Note: Sequential tasks may be executed on different
-  // threads, but strictly in sequential order.
+  // exactly in the order which they were spawned and
+  // on the thread with index 0.
   void spawn(std::function<void()> f, bool Sequential = false);
 
   void sync() const { L.sync(); }
diff --git a/llvm/lib/Support/Parallel.cpp b/llvm/lib/Support/Parallel.cpp
index a3ef3d9c621b98..3ef3b65153937a 100644
--- a/llvm/lib/Support/Parallel.cpp
+++ b/llvm/lib/Support/Parallel.cpp
@@ -113,7 +113,7 @@ class ThreadPoolExecutor : public Executor {
 
 private:
   bool hasSequentialTasks() const {
-    return !WorkQueueSequential.empty() && !SequentialQueueIsLocked;
+    return !WorkQueueSequential.empty() && (getThreadIndex() == 0);
   }
 
   bool hasGeneralTasks() const { return !WorkQueue.empty(); }
@@ -128,24 +128,15 @@ class ThreadPoolExecutor : public Executor {
       });
       if (Stop)
         break;
-      bool Sequential = hasSequentialTasks();
-      if (Sequential)
-        SequentialQueueIsLocked = true;
-      else
-        assert(hasGeneralTasks());
-
-      auto &Queue = Sequential ? WorkQueueSequential : WorkQueue;
+      auto &Queue = hasSequentialTasks() ? WorkQueueSequential : WorkQueue;
       auto Task = std::move(Queue.back());
       Queue.pop_back();
       Lock.unlock();
       Task();
-      if (Sequential)
-        SequentialQueueIsLocked = false;
     }
   }
 
   std::atomic<bool> Stop{false};
-  std::atomic<bool> SequentialQueueIsLocked{false};
   std::deque<std::function<void()>> WorkQueue;
   std::deque<std::function<void()>> WorkQueueSequential;
   std::mutex Mutex;
diff --git a/llvm/unittests/Support/ParallelTest.cpp b/llvm/unittests/Support/ParallelTest.cpp
index 0eafb9b401bee7..c6acd302945651 100644
--- a/llvm/unittests/Support/ParallelTest.cpp
+++ b/llvm/unittests/Support/ParallelTest.cpp
@@ -99,7 +99,12 @@ TEST(Parallel, TaskGroupSequentialFor) {
   {
     parallel::TaskGroup tg;
     for (size_t Idx = 0; Idx < 500; Idx++)
-      tg.spawn([&Count, Idx]() { EXPECT_EQ(Count++, Idx); }, true);
+      tg.spawn(
+          [&Count, Idx]() {
+            EXPECT_EQ(Count++, Idx);
+            EXPECT_EQ(llvm::parallel::getThreadIndex(), 0u);
+          },
+          true);
   }
   EXPECT_EQ(Count, 500ul);
 }

@@ -113,7 +113,7 @@ class ThreadPoolExecutor : public Executor {

private:
bool hasSequentialTasks() const {
return !WorkQueueSequential.empty() && !SequentialQueueIsLocked;
return !WorkQueueSequential.empty() && (getThreadIndex() == 0);
Copy link
Collaborator

Choose a reason for hiding this comment

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

IIUC, this change makes hasSequentialTasks() dependent on the thread index and hasSequentialTasks() is used as part of the Cond.wait() predicate in the worker thread main loop in work(). The add() method calls Cond.notify_one() to "wake" a single waiting worker thread. If the only outstanding task is sequential and the thread that is woken is not thread 0, then couldn't this possibly lead to the task never being executed? I think in practise because of "spurious wakeups", this would not happen but can that be guaranteed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

excuse me for the delay. I am working on this issue.

Yes, we can change the implementation to guarantee that no tasks would be missed. Will submit changes.

Comment on lines -99 to +100
// exactly in the order which they were spawned.
// Note: Sequential tasks may be executed on different
// threads, but strictly in sequential order.
// exactly in the order which they were spawned and
// on the thread with index 0.
Copy link
Collaborator

Choose a reason for hiding this comment

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

I remember this behaviour being slightly concerning when this functionality was added and now I know why...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yep.

Copy link
Member

Choose a reason for hiding this comment

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

FTR, the discussion about the "concerning" behavior is this comment ("I don't think it is a problem as far as correctness, but might be somewhat unexpected.") on https://reviews.llvm.org/D148728

@MaskRay
Copy link
Member

MaskRay commented Sep 18, 2024

Shall we revert https://reviews.llvm.org/D148728 but possibly keep the spawn helper to allow tg.spawn([]{fn();}, condition)? lld is the only spawn caller that may specify the Sequential parameter.

The downside is that we need threadIndex==0 for the main thread in the sequential case.
We cannot keep the threadIndex != UINT_MAX assertion. I feel that the value of the assertion is low.

If needed, the assertion can be restored using a different mechanism:

  • initialize threadIndex to UINT_MAX
  • make the sequential mode temporarily set threadIndex to 0, run the task, restore threadIndex

@MaskRay
Copy link
Member

MaskRay commented Sep 18, 2024

Shall we revert reviews.llvm.org/D148728 but possibly keep the spawn helper to allow tg.spawn([]{fn();}, condition)? lld is the only spawn caller that may specify the Sequential parameter.

The downside is that we need threadIndex==0 for the main thread in the sequential case. We cannot keep the threadIndex != UINT_MAX assertion. I feel that the value of the assertion is low.

If needed, the assertion can be restored using a different mechanism:

  • initialize threadIndex to UINT_MAX
  • make the sequential mode temporarily set threadIndex to 0, run the task, restore threadIndex

I think we should essentially revert https://reviews.llvm.org/D148728 . Created #109084

TaskGroup has a "sequential" mode for deterministic processing.
LLD uses this for relocations handling. But, As it places relocations
to different vectors(based on getThreadIndex), the actual locations
can be non-deterministic:

    if (shard)
      part.relrDyn->relocsVec[parallel::getThreadIndex()].push_back(
          {&isec, isec.relocs().size() - 1});

It is neccessary to use the same thread index to have the same locations.
This patch changes "sequential" mode to always execute tasks on single
thread with index 0.

Fixes this llvm#105958.
@avl-llvm avl-llvm force-pushed the avl-llvm/use-zero-thread-for-sequential branch from d848092 to b520876 Compare September 19, 2024 20:29
@avl-llvm
Copy link
Collaborator Author

avl-llvm commented Sep 19, 2024

This version of the patch is not finished. I uploaded it to show the possible solution.

@avl-llvm
Copy link
Collaborator Author

Shall we revert https://reviews.llvm.org/D148728 but possibly keep the spawn helper to allow tg.spawn([]{fn();}, condition)? lld is the only spawn caller that may specify the Sequential parameter.

The downside is that we need threadIndex==0 for the main thread in the sequential case. We cannot keep the threadIndex != UINT_MAX assertion. I feel that the value of the assertion is low.

If needed, the assertion can be restored using a different mechanism:

  • initialize threadIndex to UINT_MAX
  • make the sequential mode temporarily set threadIndex to 0, run the task, restore threadIndex

We can not temporarily set threadIndex to 0, run the task, restore threadIndex. As it could clash with another running thread using thread_index 0.

The assertion exactly prevent from the situation when we could have thread_index clashed. It seems that assertion has useful value.

The alternative solution might be to have a separate thread and condition variable for the "sequential" tasks. In this way the thread indexes would not be clashed, "sequential" tasks would have the same order and thread index. I uploaded this possible solution, though it still not fully finished.

@MaskRay
Copy link
Member

MaskRay commented Sep 19, 2024

Shall we revert reviews.llvm.org/D148728 but possibly keep the spawn helper to allow tg.spawn([]{fn();}, condition)? lld is the only spawn caller that may specify the Sequential parameter.
The downside is that we need threadIndex==0 for the main thread in the sequential case. We cannot keep the threadIndex != UINT_MAX assertion. I feel that the value of the assertion is low.
If needed, the assertion can be restored using a different mechanism:

  • initialize threadIndex to UINT_MAX
  • make the sequential mode temporarily set threadIndex to 0, run the task, restore threadIndex

We can not temporarily set threadIndex to 0, run the task, restore threadIndex. As it could clash with another running thread using thread_index 0.

The assertion exactly prevent from the situation when we could have thread_index clashed. It seems that assertion has useful value.

The alternative solution might be to have a separate thread and condition variable for the "sequential" tasks. In this way the thread indexes would not be clashed, "sequential" tasks would have the same order and thread index. I uploaded this possible solution, though it still not fully finished.

Adding to the comment #109084 (comment) : I feel that maintaining the sequential mode in Parallel.cpp adds too much complexity (WorkQueueSequential, hasSequentialTasks()).
While the overhead might be small, it is measurable (also see https://reviews.llvm.org/D148728#inline-1439381).

My current feeling is that we should restore the original state before https://reviews.llvm.org/D148728 .
The lld Relocations.cpp can be more explicit (e.g. setThreadIndex(0)) about its parallelism strategy.

@avl-llvm
Copy link
Collaborator Author

closing in favor #109084

@avl-llvm avl-llvm closed this Sep 20, 2024
@avl-llvm avl-llvm deleted the avl-llvm/use-zero-thread-for-sequential branch September 20, 2024 22:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants