################################################################################ # # Copyright (C) 2021-2022 Advanced Micro Devices, Inc. All rights reserved. # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. # ################################################################################ import os class ScriptHelper(object): """ Helper class to facilitate formatting when writing bash scripts. """ @classmethod def genLine(cls, text): return str("{0}\n").format(text) @classmethod def genComment(cls, text): return cls.genLine(str("#") + str(text)) @classmethod def genEcho(cls, text): return cls.genLine(str("echo \"{0}\";").format(str(text))) @classmethod def makeExecutable(cls, path): mode = os.stat(path).st_mode mode |= (mode & 0o444) >> 2 # copy R bits to X os.chmod(path, mode) class ScriptWriter(object): """ A script writing utility class that holds a string buffer and wraps writing to the buffer. Has factory functions to build scripts with regular workflow interface. """ def __init__(self): self._buffer = "" def writeLine(self, text): self._buffer += ScriptHelper.genLine(text) def writeComment(self, text): self._buffer += ScriptHelper.genComment(text) def writeEcho(self, text): self._buffer += ScriptHelper.genEcho(text) def dumpToFile(self, fileName, outputDir = "."): scriptPath = os.path.join(outputDir, fileName) with open(scriptPath, "w") as f: f.write(self._buffer) ScriptHelper.makeExecutable(scriptPath) def __str__(self): return self._buffer @classmethod def writeScript(bufferClass, writerClass, fileName, outputDir = "."): buffer = bufferClass() writerClass.writeHeader(buffer) writerClass.writeBody(buffer) writerClass.writeEpilogue(buffer) buffer.dumpToFile(fileName, outputDir) @classmethod def previewScript(bufferClass, writerClass): buffer = bufferClass() writerClass.writeHeader(buffer) writerClass.writeBody(buffer) writerClass.writeEpilogue(buffer) print(buffer) @classmethod def writeBenchmarkNodeScript(cls, fileName, outputDir="."): cls.writeScript(BenchmarkNodeWriter, fileName, outputDir) @classmethod def writeBenchmarkTaskScript(cls, fileName, outputDir="."): cls.writeScript(BenchmarkTaskWriter, fileName, outputDir) @classmethod def writeBenchmarkJobScript(cls, fileName, outputDir="."): cls.writeScript(BenchmarkJobWriter, fileName, outputDir) class BenchmarkNodeWriter(object): """ Node host run script. Creates an instance of the docker image and mounts the logs, results and tasks directories and finally invokes the benchmark on the supplied yaml. """ @staticmethod def __initHeader(sBuffer): sBuffer.writeComment("!/bin/bash") sBuffer.writeComment("Benchmark node script") sBuffer.writeComment("Actual host task to be run by a SLURM worker node") sBuffer.writeLine("") @staticmethod def __initializeFuncs(sBuffer): sBuffer.writeComment("Funcs") sBuffer.writeLine("usage() { echo \"Usage: $0 [-i ] [-l ] [-r ] [-t ]\" 1>&2; exit 1; }") sBuffer.writeLine("failAndExit() { echo \"FAILED: $1\" 1>&2; exit 1; }") sBuffer.writeLine("") @staticmethod def __parseArgs(sBuffer): sBuffer.writeComment("Parse arguments") sBuffer.writeLine("while getopts i:l:r:t: flag") sBuffer.writeLine("do") sBuffer.writeLine(" case \"${flag}\" in") sBuffer.writeLine(" i) dockerImagePath=${OPTARG};;") sBuffer.writeLine(" l) logDir=${OPTARG};;") sBuffer.writeLine(" r) resultDir=${OPTARG};;") sBuffer.writeLine(" t) taskDir=${OPTARG};;") sBuffer.writeLine(" *) usage;;") sBuffer.writeLine(" esac") sBuffer.writeLine("done") sBuffer.writeLine("[ -z \"${dockerImagePath}\" ] || [ -z \"${logDir}\" ] || [ -z \"${resultDir}\" ] || [ -z \"${taskDir}\" ] && usage;") sBuffer.writeLine("[ -d \"${logDir}\" ] && [ -d \"${resultDir}\" ] && [ -d \"${taskDir}\" ] || failAndExit \"Directory not found\";") sBuffer.writeLine("[ -f \"${dockerImagePath}\" ] || failAndExit \"Docker image not found: $dockerImagePath\";") sBuffer.writeEcho("Docker image path: $dockerImagePath") sBuffer.writeEcho("Path to log: $logDir") sBuffer.writeEcho("Path to task: $taskDir") sBuffer.writeEcho("Path to result: $resultDir") sBuffer.writeLine("") @staticmethod def __loadDockerImage(sBuffer): sBuffer.writeComment("Import docker image") sBuffer.writeComment("Successful output is \"Loaded image: imageName:TAG\"") sBuffer.writeEcho("Loading docker image...") sBuffer.writeLine("dockerLoadOutput=`docker load < $dockerImagePath || failAndExit \"Importing docker image\"`") sBuffer.writeLine("") sBuffer.writeEcho("Docker Load Result: $dockerLoadOutput") sBuffer.writeLine("[ -z \"${dockerLoadOutput}\" ] && failAndExit \"Loading docker image $dockerImagePath\"") sBuffer.writeLine("") sBuffer.writeComment("Split the output on ':' or spaces") sBuffer.writeComment("Capture the docker image name and tag") sBuffer.writeLine("outputSplit=(${dockerLoadOutput//:/ })") sBuffer.writeLine("dockerName=${outputSplit[2]}") sBuffer.writeLine("dockerTag=${outputSplit[3]}") sBuffer.writeLine("") sBuffer.writeComment("Find docker image record with correct name and tag") sBuffer.writeLine("dockerImageQuery=`docker images | grep -i \"$dockerName\" | grep -i \"$dockerTag\"`") sBuffer.writeLine("[ -z \"${dockerImageQuery}\" ] && failAndExit \"Finding docker image record for $dockerName:$dockerTag\"") sBuffer.writeLine("") sBuffer.writeComment("Get docker image ID") sBuffer.writeLine("outputSplit=(${dockerImageQuery// / })") sBuffer.writeLine("dockerImageId=${outputSplit[2]}") sBuffer.writeLine("[ -z \"${dockerImageId}\" ] && failAndExit \"Finding docker image id for $dockerName:$dockerTag\"") sBuffer.writeEcho("Success: Loaded Image ID: ${dockerImageId}") sBuffer.writeLine("") @staticmethod def __runTask(sBuffer): sBuffer.writeComment("Check the task dir exists") sBuffer.writeLine("") sBuffer.writeComment("Run container with mounted taskDir") sBuffer.writeEcho("Running container... (this might take some time):") sBuffer.writeLine("runCmd=\"docker run --rm --network=host --device=/dev/kfd --device=/dev/dri --group-add video --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -v \"$taskDir\":\"/TaskDir\" -v \"$resultDir\":\"/ResultDir\" -v \"$logDir\":\"/LogDir\" $dockerImageId\"") sBuffer.writeEcho("$runCmd") sBuffer.writeLine("$runCmd") sBuffer.writeLine("returnCode=$?") @staticmethod def __exit(sBuffer): sBuffer.writeEcho("Done!") sBuffer.writeLine("exit $returnCode") @classmethod def writeHeader(cls, sBuffer): cls.__initHeader(sBuffer) @classmethod def writeBody(cls, sBuffer): cls.__initializeFuncs(sBuffer) cls.__parseArgs(sBuffer) cls.__loadDockerImage(sBuffer) cls.__runTask(sBuffer) @classmethod def writeEpilogue(cls, sBuffer): cls.__exit(sBuffer) class BenchmarkTaskWriter(object): """ SLURM specific script. Invokes the 'srun' command to queue a task. SLURM environment variables tell us which indexed task to run as part of the array. The final task submitted is the node script which will run on one of the hosts. """ @staticmethod def __initHeader(sBuffer): sBuffer.writeComment("!/bin/bash") sBuffer.writeComment("Benchmark task enqueue script") sBuffer.writeComment("This script will configure and commit a single task to the cluster.") sBuffer.writeComment("Intended to be the run target of SLURM SBATCH multiplexer") sBuffer.writeLine("") @staticmethod def __initializeFuncs(sBuffer): sBuffer.writeComment("Funcs") sBuffer.writeLine("usage() { echo \"Usage: $0 [-i ] [-l ] [-r ] [-t ] \" 1>&2; exit 1; }") sBuffer.writeLine("failAndExit() { echo \"FAILED: $1\" 1>&2; exit 1; }") sBuffer.writeLine("") @staticmethod def __parseArgs(sBuffer): sBuffer.writeComment("Parse arguments") sBuffer.writeLine("while getopts i:l:r:t: flag") sBuffer.writeLine("do") sBuffer.writeLine(" case \"${flag}\" in") sBuffer.writeLine(" i) imageDir=${OPTARG};; ") sBuffer.writeLine(" l) logsDir=${OPTARG};;") sBuffer.writeLine(" r) resultsDir=${OPTARG};;") sBuffer.writeLine(" t) tasksDir=${OPTARG};;") sBuffer.writeLine(" *) usage;;") sBuffer.writeLine(" esac") sBuffer.writeLine("done") sBuffer.writeLine("[ -z \"${imageDir}\" ] || [ -z \"${logsDir}\" ] || [ -z \"${resultsDir}\" ] || [ -z \"${tasksDir}\" ] && usage;") sBuffer.writeLine("[ -d \"${imageDir}\" ] && [ -d \"${logsDir}\" ] && [ -d \"${resultsDir}\" ] && [ -d \"${tasksDir}\" ] || failAndExit \"Directory not found\";") sBuffer.writeEcho("Image dir: $imageDir") sBuffer.writeEcho("Logs dir: $logsDir") sBuffer.writeEcho("Tasks dir: $tasksDir") sBuffer.writeEcho("Results dir: $resultsDir") sBuffer.writeLine("") @staticmethod def __configureTask(sBuffer): sBuffer.writeComment("Setup task parameters") sBuffer.writeLine("pushd $tasksDir") sBuffer.writeLine("subDirs=(*)") sBuffer.writeLine("taskName=${subDirs[$SLURM_ARRAY_TASK_ID]}") sBuffer.writeLine("taskDir=$tasksDir/$taskName") sBuffer.writeLine("taskResultDir=$resultsDir/$taskName") sBuffer.writeLine("mkdir -p $taskResultDir") sBuffer.writeLine("taskLogDir=$logsDir/$taskName") sBuffer.writeLine("mkdir -p $taskLogDir") sBuffer.writeLine("task=($taskDir/*.sh)") sBuffer.writeLine("image=($imageDir/*.tar.gz)") sBuffer.writeEcho("Task ID: $SLURM_ARRAY_TASK_ID / $SLURM_TASK_MAX") sBuffer.writeEcho("Task Name: $taskName") sBuffer.writeEcho("Host Name: `hostname`") sBuffer.writeLine("") @staticmethod def __submitTask(sBuffer): sBuffer.writeComment("Enqueue node task") sBuffer.writeLine("srun -N 1 \"$task\" -i \"$image\" -l \"$taskLogDir\" -r \"$taskResultDir\" -t \"$taskDir\"") sBuffer.writeLine("returnCode=$?") @staticmethod def __exit(sBuffer): sBuffer.writeEcho("Done!") sBuffer.writeLine("exit $returnCode") @classmethod def writeHeader(cls, sBuffer): cls.__initHeader(sBuffer) @classmethod def writeBody(cls, sBuffer): cls.__initializeFuncs(sBuffer) cls.__parseArgs(sBuffer) cls.__configureTask(sBuffer) cls.__submitTask(sBuffer) @classmethod def writeEpilogue(cls, sBuffer): cls.__exit(sBuffer) class BenchmarkJobWriter(object): """ SLURM specific script. Invokes the 'sbatch' and allocates resources. Will create an array of N tasks that will each run one instance of the task script. N = number of sub-folders inside task folder Assumes one node per task. Will wait until the entire batch is complete before exiting. NOTE: the wait feature requires that all task scripts have a return code, or will hang forever! """ @staticmethod def __initHeader(sBuffer): sBuffer.writeComment("!/bin/bash") sBuffer.writeComment("Benchmark batch job script") sBuffer.writeComment("This script will configure and submit a batch job to the cluster") sBuffer.writeComment("This is the SLURM cluster benchmark entry-point") sBuffer.writeLine("") @staticmethod def __initializeFuncs(sBuffer): sBuffer.writeComment("Funcs") sBuffer.writeLine("usage() { echo \"Usage: $0 [-i ] [-l ] [-r ] [-s ] [-t ] \" 1>&2; exit 1; }") sBuffer.writeLine("failAndExit() { echo \"FAILED: $1\" 1>&2; exit 1; }") sBuffer.writeLine("") @staticmethod def __parseArgs(sBuffer): sBuffer.writeComment("Parse arguments") sBuffer.writeLine("while getopts i:l:r:s:t: flag") sBuffer.writeLine("do") sBuffer.writeLine(" case \"${flag}\" in") sBuffer.writeLine(" i) imageDir=${OPTARG};;") sBuffer.writeLine(" l) logsDir=${OPTARG};;") sBuffer.writeLine(" r) resultsDir=${OPTARG};;") sBuffer.writeLine(" s) taskScript=${OPTARG};;") sBuffer.writeLine(" t) tasksDir=${OPTARG};;") sBuffer.writeLine(" *) usage;;") sBuffer.writeLine(" esac") sBuffer.writeLine("done") sBuffer.writeLine("[ -z \"${imageDir}\" ] || [ -z \"${logsDir}\" ] || [ -z \"${resultsDir}\" ] || [ -z \"${tasksDir}\" ] && usage;") sBuffer.writeLine("[ -d \"${imageDir}\" ] && [ -d \"${logsDir}\" ] && [ -d \"${resultsDir}\" ] && [ -d \"${tasksDir}\" ] || failAndExit \"Directory not found\";") sBuffer.writeLine("[ -f \"${taskScript}\" ] || failAndExit \"Task script not found\";") sBuffer.writeEcho("Image dir: $imageDir") sBuffer.writeEcho("Logs dir: $logsDir") sBuffer.writeEcho("Tasks dir: $tasksDir") sBuffer.writeEcho("Results dir: $resultsDir") sBuffer.writeEcho("Enqueue script: $taskScript") sBuffer.writeLine("") @staticmethod def __configureJob(sBuffer): sBuffer.writeComment("Setup local environment") sBuffer.writeComment("Get in the directory where this script lives") sBuffer.writeLine("pushd $( dirname `greadlink -f ${BASH_SOURCE[0]} || readlink -f ${BASH_SOURCE[0]}` )") sBuffer.writeComment("Enumerate the tasks and set up batch params based on task count") sBuffer.writeLine("subDirs=($tasksDir/*)") sBuffer.writeLine("let arraySize=${#subDirs[@]}") sBuffer.writeLine("[ -z \"${arraySize}\" ] && failAndExit \"No task count\";") sBuffer.writeLine("let arrayStart=0") sBuffer.writeLine("let arrayEnd=$arraySize-1") sBuffer.writeComment("Create logs directory for SLURM") sBuffer.writeLine("slurmLogsDir=$logsDir/SLURM") sBuffer.writeLine("mkdir -p $slurmLogsDir") sBuffer.writeLine("") @staticmethod def __submitJob(sBuffer): sBuffer.writeComment("Invoke batch request to SLURM") sBuffer.writeComment("Will wait until the task completes before exiting") sBuffer.writeLine("runCmd=\"sbatch --nodes=1 --array=$arrayStart-$arrayEnd -o $slurmLogsDir/slurm-%A_%a.out --wait $taskScript -i $imageDir -l $logsDir -r $resultsDir -t $tasksDir\"") sBuffer.writeEcho("Submitting batch job to SLURM. This may take a while...") sBuffer.writeEcho("$runCmd") sBuffer.writeLine("$runCmd") sBuffer.writeLine("returnCode=$?") sBuffer.writeLine("popd") @staticmethod def __exit(sBuffer): sBuffer.writeEcho("Done!") sBuffer.writeLine("exit $returnCode") @classmethod def writeHeader(cls, sBuffer): cls.__initHeader(sBuffer) @classmethod def writeBody(cls, sBuffer): cls.__initializeFuncs(sBuffer) cls.__parseArgs(sBuffer) cls.__configureJob(sBuffer) cls.__submitJob(sBuffer) @classmethod def writeEpilogue(cls, sBuffer): cls.__exit(sBuffer)