#! /usr/bin/env python3 """!A shell-like syntax for running serial, MPI and OpenMP programs. This module implements a shell-like syntax for launching MPI and non-MPI programs from Python. It recognizes three types of executables: mpi, "small serial" (safe for running on a batch node) and "big serial" (which should be run via aprun if applicable). There is no difference between "small serial" and "big serial" programs except on certain architectures (like Cray) where the job script runs on a heavily-loaded batch node and has compute nodes assigned for running other programs. @section progtype Program Types There are three types of programs: mpi, serial and "big non-MPI." A "big" executable is one that is either OpenMP, or is a serial program that cannot safely be run on heavily loaded batch nodes. On Cray architecture machines, the job script runs on a heavily-populated "batch" node, with some compute nodes assigned for "large" programs. In such environments, the "big" executables are run on compute nodes and the small ones on the batch node. * mpi('exename') = an executable "exename" that calls MPI_Init and MPI_Finalize exactly once each, in that order. * exe('exename') = a small non-MPI program safe to run on a batch node * bigexe('exename') = a big non-MPI program that must be run on a compute node it may or may not use other forms of parallelism You can also make reusable aliases to avoid having to call those functions over and over (more on that later). Examples: * Python: wrf=mpi('./wrf.exe') * Python: lsl=alias(exe('/bin/ls')['-l'].env(LANG='C',LS_COLORS='never')) Those can then be reused later on as if the code is pasted in, similar to a shell alias. @section serexs Serial Execution Syntax Select your serial programs by exe('name') for small serial programs and bigexe('name') for big serial programs. The return value of those functions can then be used with a shell-like syntax to specify redirection and piping. Example: * shell version: ls -l / | wc -l * Python version: run(exe('ls')['-l','/'] | exe('wc')['-l']) Redirection syntax similar to the shell (< > and << operators): @code run( ( exe('myprogram')['arg1','arg2','...'] < 'infile' ) > 'outfile') @endcode Note the extra set of parentheses: you cannot do "exe('prog') < infile > outfile" because of the order of precedence of Python operators Append also works: @code run(exe('myprogram')['arg1','arg2','...'] >> 'appendfile') @endcode You can also send strings as input with << @code run(exe('myprogram')['arg1','arg2','...'] << 'some input string') @endcode One difference from shells is that < and << always modify the beginning of the pipeline: * shell: cat < infile | wc -l * Python #1: ( exe('cat') < 'infile' ) | exe('wc')['-l'] * Python #2: exe('cat') | ( exe('wc')['-l'] < 'infile' ) Note that the last second one, equivalent to `cat|wc -l4.99999e6: # babble about running processes if the sleep time is long. logger.info("%s is still running"%(repr(proc),)) p2.add(proc) p=p2 if not p: break # done! no need to sleep... if usleep>4.99999e6 and logger is not None: # babble about sleeping if the sleep time is 5sec or longer: logger.info("... sleep %f ..."%(float(usleep/1.e6),)) time.sleep(usleep/1.e6) return False if(p) else True def runsync(logger=None,mpiimpl=None): """!Runs the "sync" command as an exe().""" if mpiimpl is None: mpiimpl=detect_mpi() return mpiimpl.runsync(logger=logger) def run(arg,logger=None,sleeptime=None,**kwargs): """!Executes the specified program and attempts to return its exit status. In the case of a pipeline, the highest exit status seen is returned. For MPI programs, exit statuses are unreliable and generally implementation-dependent, but it is usually safe to assume that a program that runs MPI_Finalize() and exits normally will return 0, and anything that runs MPI_Abort(MPI_COMM_WORLD) will return non-zero. Programs that exit due to a signal will return statuses >255 and can be interpreted with WTERMSIG, WIFSIGNALLED, etc. @param arg the produtil.prog.Runner to execute (output of exe(), bigexe() or mpirun() @param logger a logging.Logger to log messages @param sleeptime time to sleep between checks of child process @param kwargs ignored""" p=make_pipeline(arg,False,logger=logger) p.communicate(sleeptime=sleeptime) result=p.poll() if logger is not None: logger.info(' - exit status %d'%(int(result),)) return result def checkrun(arg,logger=None,**kwargs): """!This is a simple wrapper round run that raises ExitStatusException if the program exit status is non-zero. @param arg the produtil.prog.Runner to execute (output of exe(), bigexe() or mpirun() @param logger a logging.Logger to log messages @param kwargs The optional run=[] argument can provide a different list of acceptable exit statuses.""" r=run(arg,logger=logger) if kwargs is not None and 'ret' in kwargs: if not r in kwargs['ret']: raise ExitStatusException('%s: unexpected exit status'%(repr(arg),),r) elif not r==0: raise ExitStatusException('%s: non-zero exit status'%(repr(arg),),r) return r def openmp(arg,threads=None,mpiimpl=None): """!Sets the number of OpenMP threads for the specified program. @warning Generally, when using MPI with OpenMP, the batch system must be configured correctly to handle this or unexpected errors will result. @param arg The "arg" argument must be from mpiserial, mpi, exe or bigexe. @param threads The optional "threads" argument is an integer number of threads. If it is not specified, the maximum possible number of threads will be used. Note that using threads=None with mpirun(...,allranks=True) will generally not work unless the batch system has already configured the environment correctly for an MPI+OpenMP task with default maximum threads and ranks. @returns see run()""" if mpiimpl is None: mpiimpl=detect_mpi() return mpiimpl.openmp(arg,threads) def runstr(arg,logger=None,**kwargs): """!Executes the specified program or pipeline, capturing its stdout and returning that as a string. If the exit status is non-zero, then NonZeroExit is thrown. Example: @code runstr(exe('false'),ret=(1)) @endcode succeeds if "false" returns 1, and raises ExitStatusError otherwise. @param arg The "arg" argument must be from mpiserial, mpi, exe or bigexe. @param logger a logging.Logger for logging messages @param kwargs You can specify an optional list or tuple "ret" that contains an alternative list of valid return codes. All return codes are zero or positive: negative values represent signal-terminated programs (ie.: SIGTERM produces -15, SIGKILL produces -9, etc.) """ p=make_pipeline(arg,True,logger=logger) s=p.to_string() r=p.poll() if kwargs is not None and 'ret' in kwargs: if not r in kwargs['ret']: raise ExitStatusException('%s: unexpected exit status'%(repr(arg),),r) elif not r==0: raise ExitStatusException('%s: non-zero exit status'%(repr(arg),),r) return s def mpi(arg,**kwargs): """!Returns an MPIRank object that represents the specified MPI executable. @param arg the MPI program to run @param kwargs logger=L for a logging.Logger to log messages""" return mpiprog.MPIRank(arg,**kwargs) def mpiserial(arg,**kwargs): """!Generates an mpiprog.MPISerial object that represents an MPI rank that executes a serial (non-MPI) program. The given value MUST be from bigexe() or exe(), NOT from mpi(). @param arg the MPI program to run @param kwargs logger=L for a logging.Logger to log messages""" return mpiprog.MPISerial(arg.remove_prerun(),**kwargs)