Skip to content

[SYCL] Enable parallel execution of llvm-foreach commands under an option #4360

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

Merged
merged 11 commits into from
Sep 8, 2021
Merged
38 changes: 30 additions & 8 deletions llvm/test/tools/llvm-foreach/llvm-foreach-lin.ll
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,45 @@
; RUN: echo 'Content of second file' > %t2.tgt
; RUN: echo "%t1.tgt" > %t.list
; RUN: echo "%t2.tgt" >> %t.list
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: FileCheck < %t.res %s
; CHECK: [[FIRST:.+1.tgt]]
; CHECK: [[SECOND:.+2.tgt]]
;
; CHECK-DAG: [[FIRST:.+1.tgt]]
; CHECK-DAG: [[SECOND:.+2.tgt]]

; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: FileCheck < %t.res %s --check-prefix=CHECK-ORDER
; CHECK-ORDER: [[FIRST:.+1.tgt]]
; CHECK-ORDER: [[SECOND:.+2.tgt]]

; RUN: llvm-foreach --in-replace="{}" --out-replace=%t --out-ext=out --in-file-list=%t.list --out-file-list=%t.out.list -- cp "{}" %t
; RUN: FileCheck < %t.out.list %s --check-prefix=CHECK-LIST
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --out-replace=%t --out-ext=out --in-file-list=%t.list --out-file-list=%t.out.list -- cp "{}" %t
; RUN: FileCheck < %t.out.list %s --check-prefix=CHECK-LIST
; CHECK-LIST: [[FIRST:.+\.out]]
; CHECK-LIST: [[SECOND:.+\.out]]
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.out.list -- FileCheck --input-file="{}" %s --check-prefix=CHECK-CONTENT
; CHECK-CONTENT: Content of
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.out.list -- cat "{}" > %t.order
; RUN: FileCheck < %t.order %s --check-prefix=CHECK-CONTENT
; CHECK-CONTENT: Content of first file
; CHECK-CONTENT-NEXT: Content of second file

; RUN: echo 'something' > %t3.tgt
; RUN: echo 'something again' > %t4.tgt
; RUN: echo "%t3.tgt" > %t1.list
; RUN: echo "%t4.tgt" >> %t1.list
; RUN: llvm-foreach --in-replace="{}" --in-replace="inrep" --in-file-list=%t.list --in-file-list=%t1.list --out-increment="%t_out.prj" -- echo -first-part-of-arg={}.out -first-part-of-arg=inrep.out -another-arg=%t_out.prj > %t1.res
; RUN: FileCheck < %t1.res %s --check-prefix=CHECK-DOUBLE-LISTS
; CHECK-DOUBLE-LISTS: -first-part-of-arg=[[FIRST:.+1.tgt.out]] -first-part-of-arg=[[THIRD:.+3.tgt.out]] -another-arg={{.+}}_out.prj
; CHECK-DOUBLE-LISTS: -first-part-of-arg=[[SECOND:.+2.tgt.out]] -first-part-of-arg=[[FOURTH:.+4.tgt.out]] -another-arg={{.+}}_out.prj_1
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --in-replace="inrep" --in-file-list=%t.list --in-file-list=%t1.list --out-increment="%t_out.prj" -- echo -first-part-of-arg={}.out -first-part-of-arg=inrep.out -another-arg=%t_out.prj > %t1.res
; RUN: FileCheck < %t1.res %s --check-prefix=CHECK-DOUBLE-LISTS
; CHECK-DOUBLE-LISTS-DAG: -first-part-of-arg=[[FIRST:.+1.tgt.out]] -first-part-of-arg=[[THIRD:.+3.tgt.out]] -another-arg={{.+}}_out.prj
; CHECK-DOUBLE-LISTS-DAG: -first-part-of-arg=[[SECOND:.+2.tgt.out]] -first-part-of-arg=[[FOURTH:.+4.tgt.out]] -another-arg={{.+}}_out.prj_1

; RUN: echo "%t1.tgt" > %t2.list
; RUN: echo "%t2.tgt" >> %t2.list
; RUN: echo "%t3.tgt" >> %t2.list
; RUN: echo "%t4.tgt" >> %t2.list
; RUN: llvm-foreach -j 2 --in-replace="{}" --in-file-list=%t2.list -- echo "{}" > %t2.res
; RUN: FileCheck < %t2.res %s --check-prefix=CHECK-PARALLEL-JOBS
; CHECK-PARALLEL-JOBS-DAG: [[FIRST:.+1.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[SECOND:.+2.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[THIRD:.+3.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[FOURTH:.+4.tgt]]
38 changes: 30 additions & 8 deletions llvm/test/tools/llvm-foreach/llvm-foreach-win.ll
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,45 @@
; RUN: echo 'Content of second file' > %t2.tgt
; RUN: echo "%t1.tgt" > %t.list
; RUN: echo "%t2.tgt" >> %t.list
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: FileCheck < %t.res %s
; CHECK: [[FIRST:.+1.tgt]]
; CHECK: [[SECOND:.+2.tgt]]
;
; CHECK-DAG: [[FIRST:.+1.tgt]]
; CHECK-DAG: [[SECOND:.+2.tgt]]

; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.list -- echo "{}" > %t.res
; RUN: FileCheck < %t.res %s --check-prefix=CHECK-ORDER
; CHECK-ORDER: [[FIRST:.+1.tgt]]
; CHECK-ORDER: [[SECOND:.+2.tgt]]

; RUN: llvm-foreach --in-replace="{}" --out-replace=%t --out-ext=out --in-file-list=%t.list --out-file-list=%t.out.list -- xcopy /y "{}" %t
; RUN: FileCheck < %t.out.list %s --check-prefix=CHECK-LIST
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --out-replace=%t --out-ext=out --in-file-list=%t.list --out-file-list=%t.out.list -- xcopy /y "{}" %t
; RUN: FileCheck < %t.out.list %s --check-prefix=CHECK-LIST
; CHECK-LIST: [[FIRST:.+\.out]]
; CHECK-LIST: [[SECOND:.+\.out]]
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.out.list -- FileCheck --input-file="{}" %s --check-prefix=CHECK-CONTENT
; CHECK-CONTENT: Content of
; RUN: llvm-foreach --in-replace="{}" --in-file-list=%t.out.list -- type "{}" > %t.order
; RUN: FileCheck < %t.order %s --check-prefix=CHECK-CONTENT
; CHECK-CONTENT: Content of first file
; CHECK-CONTENT-NEXT: Content of second file

; RUN: echo 'something' > %t3.tgt
; RUN: echo 'something again' > %t4.tgt
; RUN: echo "%t3.tgt" > %t1.list
; RUN: echo "%t4.tgt" >> %t1.list
; RUN: llvm-foreach --in-replace="{}" --in-replace="inrep" --in-file-list=%t.list --in-file-list=%t1.list --out-increment=%t_out.prj -- echo -first-part-of-arg={}.out -first-part-of-arg=inrep.out -another-arg=%t_out.prj > %t1.res
; RUN: FileCheck < %t1.res %s --check-prefix=CHECK-DOUBLE-LISTS
; CHECK-DOUBLE-LISTS: -first-part-of-arg=[[FIRST:.+1.tgt.out]] -first-part-of-arg=[[THIRD:.+3.tgt.out]] -another-arg={{.+}}_out.prj
; CHECK-DOUBLE-LISTS: -first-part-of-arg=[[SECOND:.+2.tgt.out]] -first-part-of-arg=[[FOURTH:.+4.tgt.out]] -another-arg={{.+}}_out.prj_1
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --in-replace="inrep" --in-file-list=%t.list --in-file-list=%t1.list --out-increment=%t_out.prj -- echo -first-part-of-arg={}.out -first-part-of-arg=inrep.out -another-arg=%t_out.prj > %t1.res
; RUN: FileCheck < %t1.res %s --check-prefix=CHECK-DOUBLE-LISTS
; CHECK-DOUBLE-LISTS-DAG: -first-part-of-arg=[[FIRST:.+1.tgt.out]] -first-part-of-arg=[[THIRD:.+3.tgt.out]] -another-arg={{.+}}_out.prj
; CHECK-DOUBLE-LISTS-DAG: -first-part-of-arg=[[SECOND:.+2.tgt.out]] -first-part-of-arg=[[FOURTH:.+4.tgt.out]] -another-arg={{.+}}_out.prj_1

; RUN: echo "%t1.tgt" > %t2.list
; RUN: echo "%t2.tgt" >> %t2.list
; RUN: echo "%t3.tgt" >> %t2.list
; RUN: echo "%t4.tgt" >> %t2.list
; RUN: llvm-foreach --jobs=2 --in-replace="{}" --in-file-list=%t2.list -- echo "{}" > %t2.res
; RUN: FileCheck < %t2.res %s --check-prefix=CHECK-PARALLEL-JOBS
; CHECK-PARALLEL-JOBS-DAG: [[FIRST:.+1.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[SECOND:.+2.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[THIRD:.+3.tgt]]
; CHECK-PARALLEL-JOBS-DAG: [[FOURTH:.+4.tgt]]
4 changes: 2 additions & 2 deletions llvm/test/tools/llvm-foreach/retain-return-val.c
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@
// RUN: chmod 777 %t2.sh
// RUN: %t2.sh
// RUN: FileCheck < %t.res %s
// CHECK: Content of first file
// CHECK: Content of second file
// CHECK-DAG: Content of first file
// CHECK-DAG: Content of second file
// CHECK: 21
74 changes: 65 additions & 9 deletions llvm/tools/llvm-foreach/llvm-foreach.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#include "llvm/Support/Path.h"
#include "llvm/Support/Program.h"
#include "llvm/Support/SystemUtils.h"
#include "llvm/Support/Threading.h"

#include <list>
#include <vector>

using namespace llvm;
Expand Down Expand Up @@ -69,6 +71,17 @@ static cl::opt<std::string> OutIncrement{
"pass."),
cl::init(""), cl::value_desc("R")};

static cl::opt<unsigned int> JobsInParallel{
"jobs",
cl::Optional,
cl::init(1),
cl::desc("Specify the number of threads for launching input commands in "
"parallel mode"),
};

static cl::alias JobsInParallelShort{"j", cl::desc("Alias for --jobs"),
cl::aliasopt(JobsInParallel)};

static void error(const Twine &Msg) {
errs() << "llvm-foreach: " << Msg << '\n';
exit(1);
Expand All @@ -79,6 +92,32 @@ static void error(std::error_code EC, const Twine &Prefix) {
error(Prefix + ": " + EC.message());
}

// With BlockingWait=false this function just goes through the all
// submitted jobs to check if some of them have finished.
int checkIfJobsAreFinished(std::list<sys::ProcessInfo> &JobsSubmitted,
bool BlockingWait = true) {
std::string ErrMsg;
auto It = JobsSubmitted.begin();
while (It != JobsSubmitted.end()) {
sys::ProcessInfo WaitResult =
sys::Wait(*It, 0, /*WaitUntilTerminates*/ BlockingWait, &ErrMsg);

// Check if the job has finished (PID will be 0 if it's not).
if (!BlockingWait && !WaitResult.Pid) {
It++;
continue;
}
assert(BlockingWait || WaitResult.Pid);
It = JobsSubmitted.erase(It);

if (WaitResult.ReturnCode != 0) {
errs() << "llvm-foreach: " << ErrMsg << '\n';
Copy link
Contributor

Choose a reason for hiding this comment

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

Ideally, we should have launched command in the error message so it could be launched standalone to debug it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good idea! We should maintain a map with the process id and the executed command (didn't find a way to get this info just from the process id) to support this with the parallel execution. I'll do it in a separate PR after this one is merged.

return WaitResult.ReturnCode;
}
}
return 0;
}

int main(int argc, char **argv) {
cl::ParseCommandLineOptions(
argc, argv,
Expand Down Expand Up @@ -160,6 +199,16 @@ int main(int argc, char **argv) {
PrevNumOfLines = FileLists[i].size();
}

if (!JobsInParallel)
error("Number of parallel threads should be a positive integer");

size_t MaxSafeNumThreads = optimal_concurrency().compute_thread_count();
if (JobsInParallel > MaxSafeNumThreads) {
JobsInParallel = MaxSafeNumThreads;
outs() << "llvm-foreach: adjusted number of threads to "
<< MaxSafeNumThreads << " (max safe available).\n";
}

std::error_code EC;
raw_fd_ostream OS{OutputFileList, EC, sys::fs::OpenFlags::OF_None};
if (!OutputFileList.empty())
Expand All @@ -170,6 +219,7 @@ int main(int argc, char **argv) {
std::string IncOutArg;
std::vector<std::string> ResInArgs(InReplaceArgs.size());
std::string ResFileList = "";
std::list<sys::ProcessInfo> JobsSubmitted;
for (size_t j = 0; j != FileLists[0].size(); ++j) {
for (size_t i = 0; i < InReplaceArgs.size(); ++i) {
ArgumentReplace CurReplace = InReplaceArgs[i];
Expand Down Expand Up @@ -221,17 +271,23 @@ int main(int argc, char **argv) {
Args[OutIncrementArg.ArgNum] = IncOutArg;
}

std::string ErrMsg;
// TODO: Add possibility to execute commands in parallel.
int Result =
sys::ExecuteAndWait(Prog, Args, /*Env=*/None, /*Redirects=*/None,
/*SecondsToWait=*/0, /*MemoryLimit=*/0, &ErrMsg);
if (Result != 0) {
errs() << "llvm-foreach: " << ErrMsg << '\n';
Res = Result;
}
// Do not start execution of a new job until previous one(s) are finished,
// if the maximum number of parallel workers is reached.
while (JobsSubmitted.size() == JobsInParallel)
if (int Result =
checkIfJobsAreFinished(JobsSubmitted, /*BlockingWait*/ false))
Res = Result;

JobsSubmitted.emplace_back(sys::ExecuteNoWait(
Prog, Args, /*Env=*/None, /*Redirects=*/None, /*MemoryLimit=*/0));
Copy link
Contributor

Choose a reason for hiding this comment

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

If Res above is not zero, new jobs should not probably be started. Instead, all running could be killed and the tool would just exit with non-zero.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, it's a good thought, but it was done on purpose in c69a311:

"we also want to allow for the compilation to continue so the user can use the generated binary knowing the timing issue as stated.").

I believe I have a bug here (should not assign the result to Res if it was successful), I'll apply the fix.

Copy link
Contributor

Choose a reason for hiding this comment

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

"we also want to allow for the compilation to continue so the user can use the generated binary knowing the timing issue as stated."

checkIfJobsAreFinished may return non-zero for a variety of reasons, not only due to a timeout. In fact timeout is not even possible, AFAICS, as SecondsToWait for sys::Wait is 0. So I believe the most likely reason for checkIfJobsAreFinished non-zero return is some error during tool execution, in which case the results will be discarded, and it does not seem to make sense to run any other tools after that.

Copy link
Contributor

Choose a reason for hiding this comment

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

I suggest that we discuss that matter separately and apply some changes (if needed) through a separate PR: apparently there was a decision to not to stop submitting further jobs if one of them failed when we executed them in serial manner. It is not clear to me why we shouldn't follow the same logic for parallel execution.

Submitting further jobs after the first failed also allows us to detect all issues in all submitted jobs and not just in the first one.

Copy link
Contributor

Choose a reason for hiding this comment

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

Submitting further jobs after the first failed also allows us to detect all issues in all submitted jobs and not just in the first one.

that does not seem right to me, and can be resource-costly approach. All jobs in a llvm-foreach batch are similar and in most cases they will fail for the same reason, I believe.

}

// Wait for all commands to be executed.
while (!JobsSubmitted.empty())
if (int Result =
checkIfJobsAreFinished(JobsSubmitted, /*BlockingWait*/ true))
Res = Result;

if (!OutputFileList.empty()) {
OS.close();
}
Expand Down