@@ -2279,15 +2279,40 @@ def submit_tasks(
22792279 submitted.
22802280
22812281 Optionally, dask can be used to schedule and run the task pool.
2282+
2283+ :param task_pool_name: name of task pool to submit
2284+ :param block: if True, return when all tasks have been launched.
2285+ If False, return when all tasks that can be launched immediately have
2286+ been launched.
2287+ :param use_dask: if True, use dask to schedule and run the task pool
2288+ :param dask_nodes: if using dask, number of nodes to use
2289+ :param dask_ppw: if using dask, number of processes per worker to use;
2290+ if None, use number of cores per node
2291+ :param launch_interval: number of seconds to wait between launching tasks
2292+ :param use_shifter: if True, use shifter to run tasks in a container
2293+ :param shifter_args: args for running under shifter
2294+ :param dask_worker_plugin: plugin class for Dask workers
2295+ :param dask_worker_per_gpu: how many Dask workers per GPU?
2296+ :param oversubscribe: if True, oversubscribe available resources
2297+ :param hwthreads: if True, use hardware threads as the basis for
2298+ resource allocation; if False, use physical cores as the basis for
2299+ resource allocation
2300+ :returns: task return value
22822301 """
22832302 start_time = time .time ()
2284- self ._send_monitor_event ('IPS_TASK_POOL_BEGIN' , 'task_pool = %s ' % task_pool_name )
2303+ self ._send_monitor_event ('IPS_TASK_POOL_BEGIN' ,
2304+ 'task_pool = %s ' % task_pool_name )
22852305 task_pool : TaskPool = self .task_pools [task_pool_name ]
22862306 retval = task_pool .submit_tasks (
2287- block , use_dask , dask_nodes , dask_ppw , launch_interval , use_shifter , shifter_args , dask_worker_plugin , dask_worker_per_gpu , oversubscribe , hwthreads
2307+ block , use_dask , dask_nodes , dask_ppw , launch_interval ,
2308+ use_shifter , shifter_args , dask_worker_plugin ,
2309+ dask_worker_per_gpu , oversubscribe , hwthreads
22882310 )
22892311 elapsed_time = time .time () - start_time
2290- self ._send_monitor_event ('IPS_TASK_POOL_END' , 'task_pool = %s elapsed time = %.2f S' % (task_pool_name , elapsed_time ), elapsed_time = elapsed_time )
2312+ self ._send_monitor_event ('IPS_TASK_POOL_END' ,
2313+ 'task_pool = %s elapsed time = %.2f S' % (
2314+ task_pool_name , elapsed_time ),
2315+ elapsed_time = elapsed_time )
22912316 return retval
22922317
22932318 def get_finished_tasks (self , task_pool_name : str ):
@@ -2429,6 +2454,8 @@ def run_ensemble(
24292454 cores_per_instance : Optional [int ] = None ,
24302455 oversubscribe : bool = False ,
24312456 hwthreads : bool = False ,
2457+ logfile : Union [str , os .PathLike ] = None ,
2458+ errfile : Union [str , os .PathLike ] = None ,
24322459 ):
24332460 """Run ensemble of simulations given the template and variables.
24342461
@@ -2456,6 +2483,14 @@ def run_ensemble(
24562483 config file created from `template` with `?` variables replaced
24572484 with the values from `variables`.
24582485
2486+ .. seealso::
2487+ :py:func:`ipsutil.params_from_csv()` for a convenient way to generate
2488+ the `variables` dict from a csv file.)
2489+
2490+ .. note::
2491+ `logfile` and `errfile` will write to the working directory local
2492+ to the ensemble instance.
2493+
24592494 TODO be able to specify the number of cores per instance
24602495
24612496 :param template: configuration template file
@@ -2470,6 +2505,8 @@ def run_ensemble(
24702505 :param oversubscribe: Whether to allow oversubscription of nodes
24712506 when launching the ensemble runs. Default is False.
24722507 :param hwthreads: Whether to use hardware threads
2508+ :param logfile: Optional file name in which to write stdout
2509+ :param errfile: Optional file name in which to write stderr
24732510 :returns: a list of dicts mapping created subdirs to simulation names
24742511 and their parameters
24752512 """
@@ -2737,13 +2774,20 @@ def send_ensemble_instance_to_portal(ensemble_name: str, data_path: Path) -> Non
27372774 # IPS run pointed to that config file.
27382775 args = [f'--simulation={ simulation_filename } ' , f'--log={ log_file } ' , f'--platform={ platform_filename !s} ' ]
27392776
2777+ kwargs = {} # optionally add logfile and errfile
2778+ if logfile :
2779+ kwargs ['logfile' ] = logfile
2780+ if errfile :
2781+ kwargs ['errfile' ] = errfile
2782+
2783+
27402784 if self .fwk .logger .getEffectiveLevel () == logging .DEBUG :
27412785 # If we're in debug mode, then also pass the debug flag.
27422786 # May as well pass in the --verbose, too.
27432787 args .insert (1 , '--debug' )
27442788 args .insert (1 , '--verbose' )
27452789
2746- self .add_task (task_pool_name , instance [0 ], 1 , working_dir , 'ips.py' , * args )
2790+ self .add_task (task_pool_name , instance [0 ], 1 , working_dir , 'ips.py' , * args , keywords = kwargs )
27472791
27482792 try :
27492793 # Note that we *always* use Dask to run the ensemble tasks
@@ -3014,7 +3058,7 @@ def submit_dask_tasks(
30143058 dask_worker_plugin = None ,
30153059 dask_worker_per_gpu = False ,
30163060 oversubscribe = False ,
3017- hwthreads = False ,
3061+ hwthreads = False
30183062 ):
30193063 """Launch tasks in *queued_tasks* using dask.
30203064
@@ -3310,7 +3354,7 @@ def submit_tasks(
33103354 dask_worker_plugin = None ,
33113355 dask_worker_per_gpu = False ,
33123356 oversubscribe = False ,
3313- hwthreads = False ,
3357+ hwthreads = False
33143358 ):
33153359 """Launch tasks in *queued_tasks*. Finished tasks are handled before
33163360 launching new ones. If *block* is ``True``, the number of
@@ -3349,22 +3393,29 @@ def submit_tasks(
33493393 :param hwthreads: If True then use hardware threads when launching
33503394 tasks. Default is False.
33513395 :type hwthreads: bool
3396+ :returns:
33523397 """
3353-
33543398 if use_dask :
33553399 if TaskPool .dask and TaskPool .distributed and self .serial_pool :
33563400 self .dask_pool = True
33573401 if use_shifter and not self .shifter :
3358- self .services .error ('Requested to run dask within shifter but shifter not available' )
3402+ self .services .error ('Requested to run dask within shifter '
3403+ 'but shifter not available' )
33593404 raise RuntimeError ('shifter not found' )
33603405 else :
33613406 return self .submit_dask_tasks (
3362- block , dask_nodes , dask_ppw , use_shifter , shifter_args , dask_worker_plugin , dask_worker_per_gpu , oversubscribe , hwthreads
3407+ block , dask_nodes , dask_ppw , use_shifter ,
3408+ shifter_args , dask_worker_plugin ,
3409+ dask_worker_per_gpu , oversubscribe , hwthreads
33633410 )
33643411 elif not TaskPool .dask or not TaskPool .distributed :
3365- raise RuntimeError ('Requested use_dask but cannot because import dask or distributed failed' )
3412+ raise RuntimeError (
3413+ 'Requested use_dask but cannot because import dask or '
3414+ 'distributed failed' )
33663415 elif not self .serial_pool :
3367- self .services .warning ('Requested use_dask but cannot because multiple processors requested' )
3416+ self .services .warning (
3417+ 'Requested use_dask but cannot because multiple '
3418+ 'processors requested' )
33683419
33693420 submit_count = 0
33703421 # Make sure any finished tasks are handled before attempting to submit
@@ -3373,7 +3424,8 @@ def submit_tasks(
33733424 while True :
33743425 if len (self .queued_tasks ) == 0 :
33753426 break
3376- active_tasks = self .services .launch_task_pool (self .name , launch_interval )
3427+ active_tasks = self .services .launch_task_pool (self .name ,
3428+ launch_interval )
33773429 for task_name , task_id in active_tasks .items ():
33783430 self .active_tasks [task_id ] = self .queued_tasks .pop (task_name )
33793431 submit_count += 1
0 commit comments