Coverage for gamdpy/simulation/Simulation.py: 33%
495 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1import numpy as np
2import numba
3import math
4import sys
5from numba import cuda
7# gamdpy
8import gamdpy as gp
10# For type annotation
11from gamdpy.integrators import Integrator
12from gamdpy.interactions import Interaction
13from gamdpy.runtime_actions import RuntimeAction
15# TODO: to remove import above you need to add a lot of lines are the following
16#from ..simulation.get_default_compute_plan import get_default_compute_plan
17#from ..configuration.Configuration import Configuration
19# IO
20import h5py
23class Simulation():
24 """ Class for running a simulation.
26 Parameters
27 ----------
29 configuration : gamdpy.Configuration
30 The configuration to simulate.
32 interactions : list of interactions
33 Interactions such as pair potentials, bonds, external fields, etc.
35 integrator : an integrator
36 The integrator to use for the simulation.
38 runtime_actions : list of runtime actions
39 Runtime actions such as ScalarSaver, TrajectorySaver or MomentumReset
41 num_timeblocks : int
42 Number of timeblocks to run the simulation. If not 0, then steps_per_timeblock should be set.
44 steps_per_timeblock : int
45 Number of steps per timeblock.
47 compute_plan : dict
48 A dictionary with the compute plan for the simulation. If None, a default compute plan is used.
50 storage : str
51 Storage for the simulation output. Can be 'memory' or a filename with extension '.h5'.
53 verbose : bool
54 If True, print verbose output.
56 timing : bool
57 If True, timing information is saved.
60 See also
61 --------
63 :func:`gamdpy.get_default_sim`
65 """
67 def __init__(self, configuration: gp.Configuration,
68 interactions: Interaction|list[Interaction],
69 integrator: Integrator,
70 runtime_actions: list[RuntimeAction],
71 num_timeblocks, steps_per_timeblock,
72 storage: str,
73 compute_plan=None,
74 verbose=False,
75 timing=True,
76 steps_in_kernel_test=1):
78 self.configuration = configuration
79 if compute_plan == None:
80 self.compute_plan = gp.get_default_compute_plan(self.configuration)
81 else:
82 self.compute_plan = compute_plan
84 # Integrator
85 if type(interactions) == list:
86 self.interactions = interactions
87 else:
88 self.interactions = [interactions, ]
89 self.integrator = integrator
90 self.dt = self.integrator.dt
92 self.num_blocks = num_timeblocks
93 self.current_block = -1
95 self.steps_per_block = steps_per_timeblock
96 self.storage = storage
97 self.timing = timing
98 self.steps_in_kernel_test = steps_in_kernel_test
100 # Close output object if there
101 # Check https://stackoverflow.com/questions/610883/how-to-check-if-an-object-has-an-attribute
102 # Create output objects
103 if self.storage == 'memory':
104 # Creates a memory h5 file with named id(self).h5; id(self) is ensured to be unique
105 self.memory = h5py.File(f"{id(self)}.h5", "w", driver='core', backing_store=False)
106 elif isinstance(self.storage, str) and self.storage[-3:] == '.h5':
107 # The append is important for repeated istances of sim with same self.storage
108 self.memory = h5py.File(self.storage, "w")
109 else:
110 raise ValueError(f"storage needs to be either 'memory' or an hdf5 filename ending in '.h5' (got: {storage})")
112 # Save setup info
113 self.memory.attrs['dt'] = self.dt
114 if 'script_name' not in self.memory.keys():
115 script_name = sys.argv[0]
116 self.memory.attrs['script_name'] = script_name
117 if isinstance(script_name,str) and script_name != '':
118 with open(script_name, 'r') as file:
119 script_content = file.read()
120 self.memory.attrs['script_content'] = script_content
122 # Saving initial configuration
123 self.configuration.save(output=self.memory, group_name="initial_configuration", mode="w", include_topology=True)
125 self.runtime_actions = runtime_actions
127 compute_flags = None
128 for runtime_action in self.runtime_actions:
129 if runtime_action.get_compute_flags() is not None:
130 if compute_flags is not None:
131 raise ValueError('Can not handle more than one compute_flags in runtime_actions')
132 else:
133 compute_flags = runtime_action.get_compute_flags()
135 self.compute_flags = gp.get_default_compute_flags() # configuration.compute_flags
136 if compute_flags is not None:
137 # only keys present in the default are processed
138 for k in compute_flags:
139 if k in self.compute_flags:
140 self.compute_flags[k] = compute_flags[k]
141 else:
142 raise ValueError('Unknown key in compute_flags:%s' %k)
143 for k in self.compute_flags:
144 if self.compute_flags[k] and not configuration.compute_flags[k]:
145 raise ValueError('compute_flags["%s]" set for Simulation but not in Configuration' % k)
147 for runtime_action in self.runtime_actions:
148 runtime_action.setup(configuration=self.configuration, num_timeblocks=num_timeblocks,
149 steps_per_timeblock=steps_per_timeblock, output=self.memory )
151 self.vectors_list = []
152 self.scalars_list = []
153 self.simbox_data_list = []
155 self.JIT_and_test_kernel()
156 for interaction in self.interactions: # Attempt to catch (eg.) nblist errors before actually doing any simulation
157 interaction.check_datastructure_validity()
160 if self.storage and self.storage[-3:] == '.h5':
161 self.memory.close()
163 # __del__ is supposed to work also if __init__ fails. This means you can't use attributed defined in __init__
164 # https://www.algorithm.co.il/programming/python-gotchas-1-__del__-is-not-the-opposite-of-__init__/
166 def get_output(self, mode="r"):
167 if self.storage and self.storage[-3:] == '.h5':
168 output = h5py.File(self.storage, mode)
169 elif self.storage == 'memory':
170 output = self.memory
171 else:
172 #print("Warning: self.output can't recognize self.storage option, returning None")
173 output = None
174 return output
176 # might be worth looking into cache_property https://www.reddit.com/r/learnpython/comments/184kqzp/in_class_properties_defined_in_functions/
177 @property
178 def output(self):
179 return self.get_output()
181 def JIT_and_test_kernel(self, adjust_compute_plan=True):
182 while True:
183 try:
184 self.get_kernels_and_params()
185 self.integrate = self.make_integrator(self.configuration, self.integrator_kernel, self.interactions_kernel,
186 #self.output_calculator_kernel,
187 self.runtime_actions_prestep_kernel,
188 self.runtime_actions_poststep_kernel,
189 self.compute_plan, True)
190 self.configuration.copy_to_device() # By _not_ copying back to host later we dont change configuration
191 self.integrate_self(0.0, self.steps_in_kernel_test)
192 break
193 except numba.cuda.cudadrv.driver.CudaAPIError as e:
194 if not adjust_compute_plan:
195 self.compute_plan['tp'] = 0 # Signal failure to autotuner
196 break
197 #print('Failed compute_plan : ', self.compute_plan)
198 if self.compute_plan['tp'] > 1: # Most common problem tp is too big
199 self.compute_plan['tp'] -= 1 # ... so we reduce it and try again
200 elif self.compute_plan['gridsync'] == True: # Last resort: turn off gridsync
201 self.compute_plan['gridsync'] = False
202 else:
203 print(f'FAILURE. Can not handle cuda error {e}')
204 exit()
205 print('Trying adjusted compute_plan :', self.compute_plan)
207 def get_kernels_and_params(self, verbose=False):
208 # Interactions
209 self.interactions_kernel, self.interactions_params = gp.add_interactions_list(self.configuration,
210 self.interactions,
211 compute_plan=self.compute_plan,
212 compute_flags=self.compute_flags)
214 # Runtime actions
215 if self.runtime_actions:
216 self.runtime_actions_prestep_kernel, self.runtime_actions_poststep_kernel, self.runtime_actions_params = gp.add_runtime_actions_list(self.configuration,
217 self.runtime_actions,
218 compute_plan=self.compute_plan)
219 else:
220 self.runtime_actions_prestep_kernel = None
221 self.runtime_actions_poststep_kernel = None
222 self.runtime_actions_params = (0,)
224 # Integrator
225 self.integrator_params = self.integrator.get_params(self.configuration, self.interactions_params)
226 self.integrator_kernel = self.integrator.get_kernel(self.configuration, self.compute_plan, self.compute_flags, self.interactions_kernel)
228 return
230 def update_params(self, verbose=False):
231 # Interactions
232 _, self.interactions_params = gp.add_interactions_list(self.configuration,
233 self.interactions,
234 compute_plan=self.compute_plan,
235 compute_flags=self.compute_flags)
237 # Runtime actions
238 if self.runtime_actions:
239 _, _, self.runtime_actions_params = gp.add_runtime_actions_list(self.configuration,
240 self.runtime_actions,
241 compute_plan=self.compute_plan)
242 else:
243 #self.runtime_actions_kernel = None
244 self.runtime_actions_params = (0,)
246 # Integrator
247 self.integrator_params = self.integrator.get_params(self.configuration, self.interactions_params, verbose)
248 #self.integrator_kernel = self.integrator.get_kernel(self.configuration, self.compute_plan, verbose)
250 return
252 def integrate_self(self, time_zero, steps):
253 self.integrate(self.configuration.d_vectors,
254 self.configuration.d_scalars,
255 self.configuration.d_ptype,
256 self.configuration.d_r_im,
257 self.configuration.simbox.d_data,
258 self.interactions_params,
259 self.integrator_params,
260 self.runtime_actions_params,
261 np.float32(time_zero),
262 steps)
263 return
266 def make_integrator(self, configuration, integration_step, compute_interactions,# output_calculator_kernel,
267 runtime_actions_prestep_kernel, runtime_actions_poststep_kernel, compute_plan, verbose=True):
269 # Unpack parameters from configuration and compute_plan
270 D, num_part = configuration.D, configuration.N
271 pb, tp, gridsync = [compute_plan[key] for key in ['pb', 'tp', 'gridsync']]
272 num_blocks = (num_part - 1) // pb + 1
274 get_integrator_setup = getattr(self.integrator, "get_setup_kernel", None)
275 if get_integrator_setup is not None:
276 integrator_setup = get_integrator_setup(self.configuration, self.compute_plan, self.interactions_kernel)
277 self.configuration.copy_to_device()
278 integrator_setup(
279 self.configuration.d_vectors,
280 self.configuration.d_scalars,
281 self.configuration.d_r_im,
282 self.configuration.simbox.d_data,
283 self.integrator_params,
284 self.configuration.d_ptype,
285 )
287 if gridsync:
288 # Return a kernel that does 'steps' timesteps, using grid.sync to syncronize
289 @cuda.jit
290 def integrator(vectors, scalars, ptype, r_im, sim_box, interaction_params, integrator_params,
291 runtime_actions_params, time_zero, steps):
292 grid = cuda.cg.this_grid()
293 for step in range(steps + 1): # make extra step without integration, so that interactions and run_time actions called for final configuration
294 time = time_zero + step * integrator_params[0]
295 compute_interactions(grid, vectors, scalars, ptype, sim_box, interaction_params)
296 grid.sync()
297 if runtime_actions_prestep_kernel != None:
298 runtime_actions_prestep_kernel(grid, vectors, scalars, r_im, sim_box, step, runtime_actions_params)
299 grid.sync()
300 if step<steps:
301 integration_step(grid, vectors, scalars, r_im, sim_box, integrator_params, time, ptype)
302 grid.sync()
303 if runtime_actions_poststep_kernel != None:
304 runtime_actions_poststep_kernel(grid, vectors, scalars, r_im, sim_box, step, runtime_actions_params)
305 grid.sync()
307 return
309 return integrator[num_blocks, (pb, tp)]
311 else:
313 # Return a Python function that does 'steps' timesteps, using kernel calls to syncronize
314 def integrator(vectors, scalars, ptype, r_im, sim_box, interaction_params, integrator_params,
315 runtime_actions_params, time_zero, steps):
316 for step in range(steps + 1): # make extra step without integration, so that interactions and run_time actions called for final configuration
317 time = time_zero + step * integrator_params[0]
318 compute_interactions(0, vectors, scalars, ptype, sim_box, interaction_params)
319 if runtime_actions_prestep_kernel != None:
320 runtime_actions_prestep_kernel(0, vectors, scalars, r_im, sim_box, step, runtime_actions_params)
321 if step<steps:
322 integration_step(0, vectors, scalars, r_im, sim_box, integrator_params, time, ptype)
323 if runtime_actions_poststep_kernel != None:
324 runtime_actions_poststep_kernel(0, vectors, scalars, r_im, sim_box, step, runtime_actions_params)
325 return
327 return integrator
328 return
330 # simple run function
332 def run(self, verbose=True):
333 """ Run the simulation.
335 See also
336 --------
338 :func:`gamdpy.Simulation.timeblocks`
340 """
341 for _ in self.run_timeblocks():
342 if verbose:
343 print(self.status(per_particle=True))
344 if verbose:
345 print(self.summary())
347 # generator for running simulation one block at a time
348 def run_timeblocks(self, num_timeblocks=-1):
349 """ Generator for running the simulation one block at a time.
351 Parameters
352 ----------
354 num_timeblocks : int
355 Number of blocks to run. If -1, all blocks are run.
357 Examples
358 --------
360 >>> import gamdpy as gp
361 >>> sim = gp.get_default_sim()
362 >>> for block in sim.run_timeblocks(num_timeblocks=3):
363 ... print(f'{block=}') # Replace with code to analyze the current configuration
364 block=0
365 block=1
366 block=2
368 See also
369 --------
371 :func:`gamdpy.Simulation.run`
373 """
374 if num_timeblocks == -1:
375 num_timeblocks = self.num_blocks
376 self.last_num_blocks = num_timeblocks
377 assert (num_timeblocks <= self.num_blocks) # Could be made OK with more blocks
379 self.configuration.copy_to_device()
380 self.vectors_list = []
381 self.scalars_list = []
382 self.simbox_data_list = []
383 self.scalars_t = []
385 if self.timing:
386 start = cuda.event()
387 end = cuda.event()
388 start_block = cuda.event()
389 end_block = cuda.event()
390 block_times = []
391 start.record()
393 zero = np.float32(0.0)
395 for block in range(num_timeblocks):
397 self.current_block = block
398 for runtime_action in self.runtime_actions:
399 runtime_action.initialize_before_timeblock(block, self.get_output(mode="a"))
401 if self.timing:
402 start_block.record()
404 self.integrate_self(np.float32(block * self.steps_per_block * self.dt),self.steps_per_block)
406 if self.timing:
407 end_block.record()
408 end_block.synchronize()
409 block_times.append(cuda.event_elapsed_time(start_block, end_block))
411 self.configuration.copy_to_host()
413 for interaction in self.interactions:
414 interaction.check_datastructure_validity()
416 for runtime_action in self.runtime_actions:
417 runtime_action.update_at_end_of_timeblock(block, self.get_output(mode="a"))
419 #if self.storage:
420 # self.configuration.save(output=self.get_output(mode="a"), group_name=f"/restarts/restart{block:04d}", mode="w", include_topology=True)
422 if self.storage and self.storage[-3:] == '.h5':
423 self.output.close()
425 yield block
427 # Finalizing run
428 if self.timing:
429 end.record()
430 end.synchronize()
432 self.timing_numba = cuda.event_elapsed_time(start, end)
433 self.timing_numba_blocks = np.array(block_times)
434 #self.nbflag = self.interactions[0].nblist.d_nbflag.copy_to_host()
435 self.scalars_list = np.array(self.scalars_list)
437 def status(self, per_particle=False) -> str:
438 """ String with the current status
439 Should be executed during the simulation run, see :func:`gamdpy.Simulation.timeblocks`
441 Parameters
442 ----------
444 per_particle : bool
445 If True, the values are divided by the number of particles in the configuration.
447 Returns
448 -------
450 str
451 A string with the current status of the simulation.
453 """
454 time = (self.current_block+1) * self.steps_per_block * self.dt
455 st = f'timeblock= {self.current_block :<6}'
456 st += f'{time= :<12.3f}'
457 for name in self.configuration.sid:
458 if name in self.configuration.compute_flags and self.configuration.compute_flags[name]:
459 data = np.sum(self.configuration[name])
460 if per_particle:
461 data /= self.configuration.N
462 st += f'{name}= {data:<10.3f}'
463 return st
465 def summary(self) -> str:
466 """ Returns a summary string of the simulation run.
467 Should be called after the simulation has been run,
468 see :func:`gamdpy.Simulation.timeblocks` or :func:`gamdpy.Simulation.run`
469 """
470 if self.timing:
471 time_total = self.timing_numba / 1000
472 tps_total = self.last_num_blocks * self.steps_per_block / time_total
473 time_sim = np.sum(self.timing_numba_blocks) / 1000
474 tps_sim = self.last_num_blocks * self.steps_per_block / time_sim
476 st = f'Particles : {self.configuration.N} \n'
477 st += f'Steps : {self.last_num_blocks} * {self.steps_per_block} = '
478 st += f'{self.last_num_blocks * self.steps_per_block:_} \n'
479 if self.timing:
480 st += f'Total run time (incl. time spent between timeblocks): {time_total:.2f} s '
481 st += f'( TPS: {tps_total:.2e} )\n'
482 st += f'Simulation time (excl. time spent between timeblocks): {time_sim:.2f} s '
483 st += f'( TPS: {tps_sim:.2e} )\n'
484 return st
486 def autotune_bruteforce(self, pbs='auto', skins='auto', tps='auto', timesteps=0, repeats=1, verbose=False):
487 if verbose:
488 print('compute_plan :', self.compute_plan)
489 if timesteps==0:
490 timesteps = self.steps_per_block
491 assert timesteps<=self.steps_per_block
493 pb = self.compute_plan['pb']
494 if pbs=='auto':
495 pbs = [pb//2, pb, pb*2]
496 if pbs=='default':
497 pbs = [pb,]
498 if verbose:
499 print('pbs :', pbs)
501 tp = self.compute_plan['tp']
502 if tps=='auto':
503 tps = [tp - 3, tp - 2, tp - 1, tp, tp + 1, tp + 2, tp + 3,]
504 if tps=='default':
505 tps = [tp,]
506 if verbose:
507 print('tps :', tps)
509 skin = self.compute_plan['skin']
510 if skins=='auto':
511 skins = [skin - 0.6, skin - 0.4, skin - 0.2, skin, skin + 0.2, skin + 0.4, skin + 0.6, skin + 0.8, skin + 1.0]
512 elif skins=='default':
513 skins = [skin, ]
514 if verbose:
515 print('skins :', skins)
517 nblists = []
518 if self.configuration.N < 32000:
519 nblists.append('N squared')
520 if self.configuration.N > 2000:
521 nblists.append('linked lists')
522 if verbose:
523 print('nblists:', nblists)
525 gridsyncs = []
526 if self.configuration.N < 200000:
527 gridsyncs.append(True)
528 if self.configuration.N > 10000:
529 gridsyncs.append(False)
530 if verbose:
531 print('gridsyncs :', gridsyncs)
533 UtilizeNIIIs = [False, True]
534 if verbose:
535 print('UtilizeNIIIs :', UtilizeNIIIs)
537 flag = cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS
538 cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS = False
540 skin_times = []
541 total_min_time = 1e9
542 optimal_compute_plan = gp.get_default_compute_plan(self.configuration) # Overwritten below
544 for nblist in nblists:
545 self.compute_plan['nblist'] = nblist
546 for gridsync in gridsyncs:
547 self.compute_plan['gridsync'] = gridsync
548 for UtilizeNIII in UtilizeNIIIs:
549 self.compute_plan['UtilizeNIII'] = UtilizeNIII
550 print(f'\n {nblist}, {gridsync=}, {UtilizeNIII=}: ', end='')
551 local_min_time = 1e9
552 for pb in pbs:
553 if pb <= 512:
554 self.compute_plan['pb'] = pb
555 for tp in tps:
556 if tp>0:
557 self.compute_plan['tp'] = tp
558 gridsync = self.compute_plan['gridsync']
559 self.JIT_and_test_kernel(adjust_compute_plan=False)
560 # does kernel run without adjustment?
561 if self.compute_plan['tp'] != tp or self.compute_plan['gridsync'] != gridsync:
562 break
563 #print('Seems to work, so looping over skins...')
564 total_min_time, local_min_time = self.autotune_scan_skin(self.compute_plan, skins, timesteps, repeats, total_min_time, local_min_time, optimal_compute_plan, verbose)
566 self.compute_plan = optimal_compute_plan
567 if verbose:
568 print('\nFinal compute_plan :', self.compute_plan)
569 else:
570 print('')
571 #print('\nFinal compute_plan :', self.compute_plan)
572 self.JIT_and_test_kernel()
574 cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS = flag
577 def autotune_scan_skin(self, compute_plan, skins, timesteps, repeats, total_min_time, local_min_time, optimal_compute_plan, verbose=False):
578 min_time = 1e9
579 skin_times = []
580 pb = compute_plan['pb']
581 tp = compute_plan['tp']
582 for skin in skins:
583 if 0 < skin < 1.2:
584 self.compute_plan['skin'] = skin
585 self.update_params()
586 self.configuration.copy_to_device() # By _not_ copying back to host later we dont change configuration
587 start = cuda.event()
588 end = cuda.event()
589 start.record()
590 for i in range(repeats):
591 self.integrate_self(0.0, timesteps)
592 end.record()
593 end.synchronize()
594 time_elapsed = cuda.event_elapsed_time(start, end)
595 if time_elapsed < min_time:
596 min_time = time_elapsed
597 min_skin = skin
598 skin_times.append(time_elapsed)
599 if verbose:
600 print(f"({skin}, {skin_times[-1]:.3})", end=' ', flush=True)
601 max_TPS = repeats * timesteps / min_time * 1000
602 if verbose:
603 print('\n', pb, tp, min_skin, min_time, max_TPS)
604 else:
605 print('.', end='', flush=True)
606 if min_time < local_min_time:
607 local_min_time = min_time
608 print(f' ({pb}x{tp},{min_skin:.2f}):{max_TPS:.2f}', end=' ', flush=True)
609 if min_time < total_min_time:
610 total_min_time = min_time
611 optimal_compute_plan['UtilizeNIII'] = self.compute_plan['UtilizeNIII']
612 optimal_compute_plan['nblist'] = self.compute_plan['nblist']
613 optimal_compute_plan['gridsync'] = self.compute_plan['gridsync']
614 optimal_compute_plan['skin'] = min_skin
615 optimal_compute_plan['pb'] = pb
616 optimal_compute_plan['tp'] = tp
617 return total_min_time, local_min_time
620 def autotune(self):
621 flag = cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS
622 cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS = False
624 initial_compute_plan = self.compute_plan.copy()
625 timesteps = self.steps_per_block
626 repeats = 1
628 # Binary choises: Take self.compute_plan as starting point, and add alternatives if appropiate
629 gridsyncs = [initial_compute_plan['gridsync'], ]
630 if gridsyncs[0] != True:
631 gridsyncs.append(True)
632 if gridsyncs[0] != False and self.configuration.N > 10000:
633 gridsyncs.append(False)
635 nblists = [initial_compute_plan['nblist'], ]
636 if nblists[0] != 'N squared' and self.configuration.N < 32000:
637 nblists.append('N squared')
638 if nblists[0] != 'linked lists' and self.configuration.N > 2000:
639 nblists.append('linked lists')
641 UtilizeNIIIs = [initial_compute_plan['UtilizeNIII'], ]
642 if UtilizeNIIIs[0] != False:
643 UtilizeNIIIs.append(False)
644 if UtilizeNIIIs[0] != True:
645 UtilizeNIIIs.append(True)
648 pb = self.compute_plan['pb']
649 pbs = [pb//2, pb, pb*2]
651 optimal_compute_plan = initial_compute_plan.copy()
652 results = []
653 # Loop over binary parameters
654 total_min_time = 1e9
655 binary_min_time = 1e9
656 for nblist in nblists:
657 self.compute_plan['nblist'] = nblist
658 for gridsync in gridsyncs:
659 self.compute_plan['gridsync'] = gridsync
660 for UtilizeNIII in UtilizeNIIIs:
661 self.compute_plan['UtilizeNIII'] = UtilizeNIII
663 # Get time for default 'pb', 'tp' to check if its worth to proceede with these choises
664 self.compute_plan['pb'] = initial_compute_plan['pb']
665 self.compute_plan['tp'] = initial_compute_plan['tp']
666 print(f'\n {nblist+",":12}\t{gridsync=}\t{UtilizeNIII=}:\t', end='')
667 self.JIT_and_test_kernel(adjust_compute_plan=False)
668 if self.compute_plan['tp'] == 0 or self.compute_plan['gridsync'] != gridsync:
669 continue
671 local_min_time = 1e9
672 total_min_time, local_min_time, min_time = self.autotune_skin(initial_compute_plan['skin'], 0.2,
673 timesteps, repeats, total_min_time, local_min_time,
674 optimal_compute_plan, verbose=False)
675 self.compute_plan['min_time'] = min_time
676 if min_time < binary_min_time:
677 binary_min_time = min_time
678 results.append(self.compute_plan.copy())
680 for compute_plan in results:
681 if compute_plan['min_time'] < 1.15 * binary_min_time:
683 local_min_time = 1e9
684 self.compute_plan = compute_plan.copy()
685 nblist = compute_plan['nblist']
686 gridsync = compute_plan['gridsync']
687 UtilizeNIII = compute_plan['UtilizeNIII']
688 print(f'\n {nblist+",":12}\t{gridsync=}\t{UtilizeNIII=}:\t', end='')
690 pb = compute_plan['pb']
691 pb_min_time = 1e9
692 initial_min_time = -1e9
693 while 4 <= pb <= 1024:
694 self.compute_plan['pb'] = pb
695 print(f' {pb=} ', end='')
696 total_min_time, local_min_time, min_time = self.autotune_tp(compute_plan['tp'], 1, compute_plan, timesteps, repeats, optimal_compute_plan, total_min_time, local_min_time)
697 if initial_min_time<0:
698 initial_min_time = min_time
699 #print(f'[{min_time:.3}, {pb_min_time:.3}]')
700 if min_time > 1.01 * pb_min_time:
701 break
702 if min_time < pb_min_time:
703 pb_min_time = min_time
704 pb *= 2
705 pb = compute_plan['pb'] // 2
706 if initial_min_time < 1.01*pb_min_time: # Is it worth trying smaller pb?
707 while 4 <= pb <= 1024:
708 self.compute_plan['pb'] = pb
709 print(f' {pb=} ', end='')
710 total_min_time, local_min_time, min_time = self.autotune_tp(compute_plan['tp'], 1, compute_plan, timesteps, repeats, optimal_compute_plan, total_min_time, local_min_time)
711 #print(f'[{min_time:.3}, {pb_min_time:.3}]')
712 if min_time > 1.01 * pb_min_time:
713 break
714 if min_time < pb_min_time:
715 pb_min_time = min_time
716 pb = pb // 2
718 self.compute_plan = optimal_compute_plan
719 self.JIT_and_test_kernel()
720 print()
721 cuda.config.CUDA_LOW_OCCUPANCY_WARNINGS = flag
724 def autotune_tp(self, initial_tp, delta_tp, initial_compute_plan, timesteps, repeats, optimal_compute_plan, total_min_time, local_min_time):
725 tp = initial_tp
726 tp_min_time = 1e9
727 while 0 < tp <= 64:
728 #print(self.compute_plan['pb'], tp, (self.compute_plan['pb']*tp) % 32)
729 if (self.compute_plan['pb']*tp) % 32 == 0:
730 self.compute_plan['tp'] = tp
731 self.JIT_and_test_kernel(adjust_compute_plan=False)
732 if self.compute_plan['tp'] != tp: #or self.compute_plan['gridsync'] != gridsync:
733 break
734 total_min_time, local_min_time, min_time = self.autotune_skin(initial_compute_plan['skin'], 0.2,
735 timesteps, repeats, total_min_time, local_min_time,
736 optimal_compute_plan, verbose=False)
737 if min_time > 1.05 * tp_min_time:
738 break
739 if min_time < tp_min_time:
740 tp_min_time = min_time
741 tp += delta_tp
742 tp = initial_tp - delta_tp
743 while 0 < tp <= 64:
744 if (self.compute_plan['pb']*tp) % 32 == 0:
745 self.compute_plan['tp'] = tp
746 self.JIT_and_test_kernel(adjust_compute_plan=False)
747 if self.compute_plan['tp'] != tp: #or self.compute_plan['gridsync'] != gridsync:
748 break
749 total_min_time, local_min_time, min_time = self.autotune_skin(initial_compute_plan['skin'], 0.2,
750 timesteps, repeats, total_min_time, local_min_time,
751 optimal_compute_plan, verbose=False)
752 if min_time > 1.05 * tp_min_time:
753 break
754 if min_time < tp_min_time:
755 tp_min_time = min_time
756 tp -= delta_tp
757 return total_min_time, local_min_time, tp_min_time
760 def autotune_skin(self, initial_skin, delta_skin, timesteps, repeats, total_min_time, local_min_time, optimal_compute_plan, verbose=False):
761 min_time = 1e9
762 min_skin = -0.1
764 # Upscan
765 min_time, min_skin = self.scan_skin(timesteps, repeats, initial_skin, delta_skin, min_time, min_skin)
767 # Downscan
768 min_time, min_skin = self.scan_skin(timesteps, repeats, initial_skin-delta_skin, -delta_skin, min_time, min_skin)
770 max_TPS = repeats * timesteps / min_time * 1000
771 if min_time < local_min_time:
772 local_min_time = min_time
773 print(f" ({self.compute_plan['pb']}x{self.compute_plan['tp']},{min_skin:.2f}):{max_TPS:.2f}", end=' ', flush=True)
774 else:
775 print('.', end='', flush=True)
776 if min_time < total_min_time:
777 total_min_time = min_time
778 optimal_compute_plan['UtilizeNIII'] = self.compute_plan['UtilizeNIII']
779 optimal_compute_plan['nblist'] = self.compute_plan['nblist']
780 optimal_compute_plan['gridsync'] = self.compute_plan['gridsync']
781 optimal_compute_plan['skin'] = min_skin
782 optimal_compute_plan['pb'] = self.compute_plan['pb']
783 optimal_compute_plan['tp'] = self.compute_plan['tp']
784 return total_min_time, local_min_time, min_time
786 def scan_skin(self, timesteps, repeats, skin, delta_skin, min_time, min_skin):
787 while 0.0 < skin < 1.45: # Should keep on going until eg. linked lists throw an error
788 self.compute_plan['skin'] = skin
789 self.update_params()
790 self.configuration.copy_to_device() # By _not_ copying back to host later we dont change configuration
791 start = cuda.event()
792 end = cuda.event()
793 start.record()
794 for i in range(repeats):
795 self.integrate_self(0.0, timesteps)
796 end.record()
797 end.synchronize()
798 time_elapsed = cuda.event_elapsed_time(start, end)
799 if time_elapsed < min_time:
800 min_time = time_elapsed
801 min_skin = skin
802 elif time_elapsed > 1.1*min_time:
803 break
804 skin += delta_skin
805 return min_time, min_skin