1818 timed_pass
1919)
2020from devito .types import Array , CustomDimension , Eq , ModuloDimension
21+ from devito .warnings import warn
2122
2223__all__ = ['buffering' ]
2324
@@ -39,7 +40,7 @@ def buffering(clusters, key, sregistry, options, **kwargs):
3940 The symbol registry, to create unique names for buffers and Dimensions.
4041 options : dict
4142 The optimization options.
42- Accepted: ['buf-async-degree'].
43+ Accepted: ['buf-async-degree', 'buf-reuse', 'npthreads' ].
4344 * 'buf-async-degree': Specify the size of the buffer. By default, the
4445 buffer size is the minimal one, inferred from the memory accesses in
4546 the ``clusters`` themselves. An asynchronous degree equals to `k`
@@ -49,6 +50,9 @@ def buffering(clusters, key, sregistry, options, **kwargs):
4950 implemented by other passes).
5051 * 'buf-reuse': If True, the pass will try to reuse existing Buffers for
5152 different buffered Functions. By default, False.
53+ * 'npthreads': Number of pthreads for asynchronous tasks. The tasks are
54+ divided into this many balanced groups. By default, None, which uses
55+ one pthread per task.
5256 **kwargs
5357 Additional compilation options.
5458 Accepted: ['opt_init_onwrite', 'opt_buffer'].
@@ -252,36 +256,41 @@ def callback(self, clusters, prefix):
252256 processed .append (Cluster (expr , ispace , guards , properties , syncs ))
253257
254258 # Lift {write,read}-only buffers into separate IterationSpaces
255- if not self .options ['fuse-tasks' ]:
256- processed = self ._optimize (processed , descriptors )
259+ processed = self ._optimize (processed , descriptors )
257260
258- if self . options [ 'buf-reuse' ]:
259- init , processed = self ._reuse (init , processed , descriptors )
261+ # Reuse existing Buffers for buffering candidates, if requested
262+ init , processed = self ._reuse (init , processed , descriptors )
260263
261264 return init + processed
262265
263266 def _optimize (self , clusters , descriptors ):
267+ npthreads = self .options ['npthreads' ]
268+
269+ stamps = self ._make_task_groups (descriptors )
270+
264271 for b , v in descriptors .items ():
265272 if v .is_writeonly :
266273 # `b` might be written by multiple, potentially mutually
267274 # exclusive, equations. For example, two equations that have or
268275 # will have complementary guards, hence only one will be
269276 # executed. In such a case, we can split the equations over
270277 # separate IterationSpaces
271- key0 = lambda : Stamp ()
278+ key0 = lambda : stamps [ b ] if npthreads else Stamp () # noqa: B023
272279 elif v .is_readonly :
273280 # `b` is read multiple times -- this could just be the case of
274281 # coupled equations, so we more cautiously perform a
275282 # "buffer-wise" splitting of the IterationSpaces (i.e., only
276283 # relevant if there are at least two read-only buffers)
277- stamp = Stamp ()
278- key0 = lambda : stamp # noqa: B023
284+ stamp_fixed = Stamp ()
285+ key0 = lambda : stamps [ b ] if npthreads else stamp_fixed # noqa: B023
279286 else :
280287 continue
281288
282289 processed = []
283290 for c in clusters :
284- if b not in c .functions :
291+ f = v .f if npthreads else b
292+
293+ if f not in c .functions :
285294 processed .append (c )
286295 continue
287296
@@ -294,12 +303,57 @@ def _optimize(self, clusters, descriptors):
294303
295304 return clusters
296305
306+ def _make_task_groups (self , descriptors ):
307+ """
308+ Assign task buffers to at most `npthreads` balanced groups.
309+ """
310+ npthreads = self .options ['npthreads' ]
311+
312+ stamps = {}
313+ if not npthreads :
314+ return stamps
315+
316+ task_sets = (
317+ [b for b , v in descriptors .items () if v .is_writeonly ],
318+ [b for b , v in descriptors .items () if v .is_readonly ],
319+ )
320+
321+ for tasks in task_sets :
322+ if not tasks :
323+ continue
324+
325+ # Calculate the number of groups and their sizes, so that the tasks
326+ # are divided into balanced groups
327+ ntasks = len (tasks )
328+ ngroups = min (npthreads , ntasks )
329+ base , remainder = divmod (ntasks , ngroups )
330+ sizes = (base + 1 ,) * remainder + (base ,) * (ngroups - remainder )
331+
332+ if npthreads > ntasks :
333+ warn (
334+ f"`npthreads={ npthreads } ` exceeds the { ntasks } available "
335+ f"tasks; using `npthreads={ ntasks } ` instead"
336+ )
337+
338+ # Create a unique Stamp for each group and assign it to the tasks
339+ # in that group
340+ start = 0
341+ for n in sizes :
342+ stamp = Stamp ()
343+ stamps .update ({b : stamp for b in tasks [start :start + n ]})
344+ start += n
345+
346+ return stamps
347+
297348 def _reuse (self , init , clusters , descriptors ):
298349 """
299350 Reuse existing Buffers for buffering candidates.
300351 """
301352 buf_reuse = self .options ['buf-reuse' ]
302353
354+ if not buf_reuse :
355+ return init , clusters
356+
303357 if callable (buf_reuse ):
304358 cbk = lambda v : [i for i in v if buf_reuse (descriptors [i ])]
305359 else :
0 commit comments