diff --git a/devito/arch/archinfo.py b/devito/arch/archinfo.py index ca3c52dd63..9681135871 100644 --- a/devito/arch/archinfo.py +++ b/devito/arch/archinfo.py @@ -734,7 +734,7 @@ def simd_items_per_reg(self, dtype): def numa_domains(self): try: return int(lscpu()['NUMA node(s)']) - except KeyError: + except (ValueError, TypeError, KeyError): warning("NUMA domain count autodetection failed") return 1 diff --git a/devito/builtins/utils.py b/devito/builtins/utils.py index df7c7ddca3..67aef28ba0 100644 --- a/devito/builtins/utils.py +++ b/devito/builtins/utils.py @@ -148,7 +148,7 @@ def wrapper(*args, **kwargs): for i in args: try: - if i.is_transient: + if not i.is_persistent: raise ValueError(f"Cannot apply `{func.__name__}` to transient " f"function `{i.name}` on backend `{platform}`") except AttributeError: diff --git a/devito/core/operator.py b/devito/core/operator.py index 39c7a61fef..e6bfd18916 100644 --- a/devito/core/operator.py +++ b/devito/core/operator.py @@ -2,7 +2,7 @@ from functools import cached_property from devito.core.autotuning import autotune -from devito.exceptions import InvalidArgument, InvalidOperator +from devito.exceptions import InvalidOperator from devito.ir import FindSymbols from devito.logger import warning from devito.mpi.routines import mpi_registry @@ -170,15 +170,15 @@ def _check_kwargs(cls, **kwargs): raise InvalidOperator("Unsupported MPI mode `%s`" % oo['mpi']) if oo['cse-algo'] not in ('basic', 'smartsort', 'advanced'): - raise InvalidArgument("Illegal `cse-algo` value") + raise InvalidOperator("Illegal `cse-algo` value") if oo['deriv-schedule'] not in ('basic', 'smart'): - raise InvalidArgument("Illegal `deriv-schedule` value") + raise InvalidOperator("Illegal `deriv-schedule` value") if oo['deriv-unroll'] not in (False, 'inner', 'full'): - raise InvalidArgument("Illegal `deriv-unroll` value") + raise InvalidOperator("Illegal `deriv-unroll` value") if oo['errctl'] not in (None, False, 'basic', 'max'): - raise InvalidArgument("Illegal `errctl` value") + raise InvalidOperator("Illegal `errctl` value") def _autotune(self, args, setup): if setup in [False, 'off']: diff --git a/devito/exceptions.py b/devito/exceptions.py index fa5619d4ca..b15c5fd32f 100644 --- a/devito/exceptions.py +++ b/devito/exceptions.py @@ -1,22 +1,56 @@ class DevitoError(Exception): - pass + """ + Base class for all Devito-related exceptions. + """ class CompilationError(DevitoError): - pass + """ + Raised by the JIT compiler when the generated code cannot be compiled, + typically due to a syntax error. + These errors typically stem by one of the following: -class InvalidArgument(DevitoError): - pass + * A flaw in the user-provided equations; + * An issue with the user-provided compiler options, not compatible + with the given equations and/or backend; + * A bug or a limitation in the Devito compiler itself. + """ -class InvalidOperator(DevitoError): - pass +class InvalidArgument(ValueError, DevitoError): + """ + Raised by the runtime system when an `op.apply(...)` argument, either a + default argument or a user-provided one ("override"), is not valid. + These are typically user-level errors, such as passing an incorrect + type of argument, or passing an argument with an incorrect value. + """ -class ExecutionError(DevitoError): - pass + +class InvalidOperator(DevitoError): + """ + Raised by the runtime system when an `Operator` cannot be constructed. + + This generally occurs when an invalid combination of arguments is supplied to + `Operator(...)` (e.g., a GPU-only optimization option is provided, while the + Operator is being generated for the CPU). + """ -class VisitorException(DevitoError): - pass +class ExecutionError(DevitoError): + """ + Raised after `op.apply(...)` if a runtime error occurred during the execution + of the Operator is detected. + + The nature of these errors can be various, for example: + + * Unstable numerical behavior (e.g., NaNs); + * Out-of-bound accesses to arrays, which in turn can be caused by: + * Incorrect user-provided equations (e.g., abuse of the "indexed notation"); + * A buggy optimization pass; + * Running out of resources: + * Memory (e.g., too many temporaries in the generated code); + * Device shared memory or registers (e.g., too many threads per block); + * etc. + """ diff --git a/devito/ir/clusters/algorithms.py b/devito/ir/clusters/algorithms.py index 1a05f0842a..5323539dd4 100644 --- a/devito/ir/clusters/algorithms.py +++ b/devito/ir/clusters/algorithms.py @@ -5,7 +5,7 @@ import numpy as np import sympy -from devito.exceptions import InvalidOperator +from devito.exceptions import CompilationError from devito.finite_differences.elementary import Max, Min from devito.ir.support import (Any, Backward, Forward, IterationSpace, erange, pull_dims, null_ispace) @@ -306,8 +306,8 @@ def callback(self, clusters, prefix): elif len(sis) == 1: si = sis.pop() else: - raise InvalidOperator("Cannot use multiple SteppingDimensions " - "to index into a Function") + raise CompilationError("Cannot use multiple SteppingDimensions " + "to index into a Function") size = i.function.shape_allocated[d] assert is_integer(size) diff --git a/devito/ir/iet/visitors.py b/devito/ir/iet/visitors.py index 505fe2e001..cd405833f5 100644 --- a/devito/ir/iet/visitors.py +++ b/devito/ir/iet/visitors.py @@ -13,7 +13,7 @@ from sympy import IndexedBase from sympy.core.function import Application -from devito.exceptions import VisitorException +from devito.exceptions import CompilationError from devito.ir.iet.nodes import (Node, Iteration, Expression, ExpressionBundle, Call, Lambda, BlankLine, Section, ListMajor) from devito.ir.support.space import Backward @@ -1188,7 +1188,7 @@ def visit_Node(self, o, **kwargs): elif isinstance(handle, Iterable): # Iterable -> inject `handle` into `o`'s children if not o.children: - raise VisitorException + raise CompilationError("Cannot inject nodes in a leaf node") if self.nested: children = [self._visit(i, **kwargs) for i in o.children] else: diff --git a/devito/operator/operator.py b/devito/operator/operator.py index 90e827de84..ccfa5a249f 100644 --- a/devito/operator/operator.py +++ b/devito/operator/operator.py @@ -11,7 +11,8 @@ from devito.arch import ANYCPU, Device, compiler_registry, platform_registry from devito.data import default_allocator -from devito.exceptions import InvalidOperator, ExecutionError +from devito.exceptions import (CompilationError, ExecutionError, InvalidArgument, + InvalidOperator) from devito.logger import debug, info, perf, warning, is_log_enabled_for, switch_log_level from devito.ir.equations import LoweredEq, lower_exprs, concretize_subdims from devito.ir.clusters import ClusterGroup, clusterize @@ -188,7 +189,8 @@ def _sanitize_exprs(cls, expressions, **kwargs): for i in expressions: if not isinstance(i, Evaluable): - raise InvalidOperator("`%s` is not an `Evaluable` object" % str(i)) + raise CompilationError(f"`{i!s}` is not an Evaluable object; " + "check your equation again") return expressions @@ -552,7 +554,7 @@ def _prepare_arguments(self, autotune=None, **kwargs): if not configuration['ignore-unknowns']: for k, v in kwargs.items(): if k not in self._known_arguments: - raise ValueError("Unrecognized argument %s=%s" % (k, v)) + raise InvalidArgument(f"Unrecognized argument `{k}={v}`") # Pre-process Dimension overrides. This may help ruling out ambiguities # when processing the `defaults` arguments. A topological sorting is used @@ -582,8 +584,10 @@ def _prepare_arguments(self, autotune=None, **kwargs): try: args.reduce_inplace() except ValueError: - raise ValueError("Override `%s` is incompatible with overrides `%s`" % - (p, [i for i in overrides if i.name in args])) + v = [i for i in overrides if i.name in args] + raise InvalidArgument( + f"Override `{p}` is incompatible with overrides `{v}`" + ) # Process data-carrier defaults for p in defaults: @@ -603,10 +607,11 @@ def _prepare_arguments(self, autotune=None, **kwargs): # `fact` is supplied w/o overriding `usave`; that's legal pass elif is_integer(args[k]) and not contains_val(args[k], v): - raise ValueError("Default `%s` is incompatible with other args as " - "`%s=%s`, while `%s=%s` is expected. Perhaps you " - "forgot to override `%s`?" % - (p, k, v, k, args[k], p)) + raise InvalidArgument( + f"Default `{p}` is incompatible with other args as " + f"`{k}={v}`, while `{k}={args[k]}` is expected. Perhaps " + f"you forgot to override `{p}`?" + ) args = kwargs['args'] = args.reduce_all() @@ -692,6 +697,18 @@ def _postprocess_errors(self, retval): raise ExecutionError("Detected nan/inf in some output Functions") elif retval == error_mapper['KernelLaunch']: raise ExecutionError("Kernel launch failed") + elif retval == error_mapper['KernelLaunchOutOfResources']: + raise ExecutionError( + "Kernel launch failed due to insufficient resources. This may be " + "due to excessive register pressure in one of the Operator " + "kernels. Try supplying a smaller `par-tile` value." + ) + elif retval == error_mapper['KernelLaunchUnknown']: + raise ExecutionError( + "Kernel launch failed due to an unknown error. This might " + "simply indicate memory corruption, but also, in a more unlikely " + "case, a hardware issue. Please report this issue to the " + "Devito team.") else: raise ExecutionError("An error occurred during execution") @@ -725,7 +742,7 @@ def arguments(self, **kwargs): # Check all arguments are present for p in self.parameters: if args.get(p.name) is None: - raise ValueError("No value found for parameter %s" % p.name) + raise InvalidArgument(f"No value found for parameter {p.name}") return args # Code generation and JIT compilation diff --git a/devito/passes/clusters/aliases.py b/devito/passes/clusters/aliases.py index ca5343c9ac..8f03246f30 100644 --- a/devito/passes/clusters/aliases.py +++ b/devito/passes/clusters/aliases.py @@ -5,6 +5,7 @@ import numpy as np import sympy +from devito.exceptions import CompilationError from devito.finite_differences import EvalDerivative, IndexDerivative, Weights from devito.ir import (SEQUENTIAL, PARALLEL_IF_PVT, SEPARABLE, Forward, IterationSpace, Interval, Cluster, ExprGeometry, Queue, @@ -372,9 +373,10 @@ def _select(self, variants): try: return variants[self.opt_schedule_strategy] except IndexError: - raise ValueError("Illegal schedule %d; " - "generated %d schedules in total" - % (self.opt_schedule_strategy, len(variants))) + raise CompilationError( + f"Illegal schedule {self.opt_schedule_strategy}; " + f"generated {len(variants)} schedules in total" + ) return pick_best(variants) diff --git a/devito/passes/clusters/buffering.py b/devito/passes/clusters/buffering.py index 5a6cddb87e..88a07816f3 100644 --- a/devito/passes/clusters/buffering.py +++ b/devito/passes/clusters/buffering.py @@ -8,7 +8,7 @@ from devito.ir import (Cluster, Backward, Forward, GuardBound, Interval, IntervalGroup, IterationSpace, Properties, Queue, Vector, InitArray, lower_exprs, vmax, vmin) -from devito.exceptions import InvalidOperator +from devito.exceptions import CompilationError from devito.logger import warning from devito.passes.clusters.utils import is_memcpy from devito.symbolics import IntDiv, retrieve_functions, uxreplace @@ -312,8 +312,8 @@ def generate_buffers(clusters, key, sregistry, options, **kwargs): dims = [d for d in f.dimensions if d not in bdims] if len(dims) != 1: - raise InvalidOperator("Unsupported multi-dimensional `buffering` " - "required by `%s`" % f) + raise CompilationError(f"Unsupported multi-dimensional `buffering` " + f"required by `{f}`") dim = dims.pop() if is_buffering(exprs): @@ -397,8 +397,8 @@ def __init__(self, f, b, clusters): ispaces = {i.lift(self.bdims, v=stamp) for i in ispaces} if len(ispaces) > 1: - raise InvalidOperator("Unsupported `buffering` over different " - "IterationSpaces") + raise CompilationError("Unsupported `buffering` over different " + "IterationSpaces") assert len(ispaces) == 1, "Unexpected form of `buffering`" self.ispace = ispaces.pop() diff --git a/devito/passes/iet/definitions.py b/devito/passes/iet/definitions.py index b5e761e28e..3532169754 100644 --- a/devito/passes/iet/definitions.py +++ b/devito/passes/iet/definitions.py @@ -168,11 +168,7 @@ def _alloc_mapped_array_on_high_bw_mem(self, site, obj, storage, *args): """ decl = Definition(obj) - # Allocating a mapped Array on the high bandwidth memory requires - # multiple statements, hence we implement it as a generic Callable - # to minimize code size, since different arrays will ultimately be - # able to reuse the same abstract Callable - + # Allocate the Array struct memptr = VOID(Byref(obj._C_symbol), '**') alignment = obj._data_alignment nbytes = SizeOf(obj._C_typedata) @@ -181,10 +177,12 @@ def _alloc_mapped_array_on_high_bw_mem(self, site, obj, storage, *args): nbytes_param = Symbol(name='nbytes', dtype=np.uint64, is_const=True) nbytes_arg = SizeOf(obj.indexed._C_typedata)*obj.size + # Allocate the underlying host data ffp0 = FieldFromPointer(obj._C_field_data, obj._C_symbol) memptr = VOID(Byref(ffp0), '**') allocs.append(self.lang['host-alloc-pin'](memptr, alignment, nbytes_param)) + # Initialize the Array struct ffp1 = FieldFromPointer(obj._C_field_nbytes, obj._C_symbol) init0 = DummyExpr(ffp1, nbytes_param) ffp2 = FieldFromPointer(obj._C_field_size, obj._C_symbol) @@ -193,8 +191,7 @@ def _alloc_mapped_array_on_high_bw_mem(self, site, obj, storage, *args): frees = [self.lang['host-free-pin'](ffp0), self.lang['host-free'](obj._C_symbol)] - # Not all backends require explicit allocation/deallocation of the - # `dmap` field + # Allocate the underlying device data, if required by the backend alloc, free = self._make_dmap_allocfree(obj, nbytes_param) # Chain together all allocs and frees @@ -203,6 +200,8 @@ def _alloc_mapped_array_on_high_bw_mem(self, site, obj, storage, *args): ret = Return(obj._C_symbol) + # Wrap everything in a Callable so that we can reuse the same code + # for equivalent Array structs name = self.sregistry.make_name(prefix='alloc') body = (decl, *allocs, init0, init1, ret) efunc0 = make_callable(name, body, retval=obj) @@ -210,6 +209,7 @@ def _alloc_mapped_array_on_high_bw_mem(self, site, obj, storage, *args): args[args.index(nbytes_param)] = nbytes_arg alloc = Call(name, args, retobj=obj) + # Same story for the frees name = self.sregistry.make_name(prefix='free') efunc1 = make_callable(name, frees) free = Call(name, efunc1.parameters) @@ -222,6 +222,7 @@ def _alloc_bundle_struct_on_high_bw_mem(self, site, obj, storage): """ decl = Definition(obj) + # Allocate the Bundle struct memptr = VOID(Byref(obj._C_symbol), '**') alignment = obj._data_alignment nbytes = SizeOf(obj._C_typedata) @@ -230,6 +231,7 @@ def _alloc_bundle_struct_on_high_bw_mem(self, site, obj, storage): nbytes_param = Symbol(name='nbytes', dtype=np.uint64, is_const=True) nbytes_arg = SizeOf(obj.indexed._C_typedata)*obj.size + # Initialize the Bundle struct ffp1 = FieldFromPointer(obj._C_field_nbytes, obj._C_symbol) init0 = DummyExpr(ffp1, nbytes_param) ffp2 = FieldFromPointer(obj._C_field_size, obj._C_symbol) @@ -239,6 +241,8 @@ def _alloc_bundle_struct_on_high_bw_mem(self, site, obj, storage): ret = Return(obj._C_symbol) + # Wrap everything in a Callable so that we can reuse the same code + # for equivalent Bundle structs name = self.sregistry.make_name(prefix='alloc') body = (decl, alloc, init0, init1, ret) efunc0 = make_callable(name, body, retval=obj) diff --git a/devito/passes/iet/errors.py b/devito/passes/iet/errors.py index 6d8f60526a..13f1101a3f 100644 --- a/devito/passes/iet/errors.py +++ b/devito/passes/iet/errors.py @@ -108,4 +108,6 @@ class Retval(LocalObject, Expr): error_mapper = { 'Stability': 100, 'KernelLaunch': 200, + 'KernelLaunchOutOfResources': 201, + 'KernelLaunchUnknown': 202, } diff --git a/devito/passes/iet/orchestration.py b/devito/passes/iet/orchestration.py index 952e1cbb6f..b807fd561b 100644 --- a/devito/passes/iet/orchestration.py +++ b/devito/passes/iet/orchestration.py @@ -3,7 +3,7 @@ from sympy import Or -from devito.exceptions import InvalidOperator +from devito.exceptions import CompilationError from devito.ir.iet import (Call, Callable, List, SyncSpot, FindNodes, Transformer, BlankLine, BusyWait, DummyExpr, AsyncCall, AsyncCallable, make_callable, derive_parameters) @@ -156,7 +156,7 @@ def process(self, iet): layers = {infer_layer(s.function) for s in sync_ops} if len(layers) != 1: - raise InvalidOperator("Unsupported streaming case") + raise CompilationError("Unsupported streaming case") layer = layers.pop() n1, v = callbacks[t](subs.get(n0, n0), sync_ops, layer) diff --git a/devito/types/basic.py b/devito/types/basic.py index e70a105296..9bdd6bc8dd 100644 --- a/devito/types/basic.py +++ b/devito/types/basic.py @@ -984,10 +984,10 @@ def __init_finalize__(self, *args, **kwargs): assert self._space in ['local', 'mapped', 'host'] # If True, the AbstractFunction is treated by the compiler as a "transient - # field", meaning that its content is only useful within an Operator - # execution, but the final data is not expected to be read back in - # Python-land by the user. This allows the compiler/run-time to apply - # certain optimizations, such as avoiding memory copies + # field", meaning that its content cannot be accessed by the user in + # Python-land. This allows the compiler/run-time to apply certain + # optimizations, such as avoiding memory copies across different Operator + # executions self._is_transient = kwargs.get('is_transient', False) # Averaging mode for off the grid evaluation @@ -1274,6 +1274,16 @@ def is_const(self): def is_transient(self): return self._is_transient + @property + def is_persistent(self): + """ + True if the AbstractFunction is persistent, i.e., its data is guaranteed + to exist across multiple Operator invocations, False otherwise. + By default, transient AbstractFunctions are not persistent. However, + subclasses may override this behavior. + """ + return not self.is_transient + @cached_property def properties(self): return frozendict([(i, getattr(self, i)) for i in self.__properties__]) diff --git a/devito/types/dense.py b/devito/types/dense.py index 7afcf6f12b..b05beb656c 100644 --- a/devito/types/dense.py +++ b/devito/types/dense.py @@ -1374,10 +1374,10 @@ def __init_finalize__(self, *args, **kwargs): available_mem = virtual_memory().available required_mem = np.dtype(self.dtype).itemsize * self.size if required_mem > available_mem: - warning("Trying to allocate more memory (%s) " - % humanbytes(required_mem) + "for symbol %s " % self.name + - "than available (%s) " % humanbytes(available_mem) + - "on physical device, this will start swapping") + raise MemoryError( + f"Trying to allocate more memory ({humanbytes(required_mem)}) " + f"for `{self.name}` than available ({humanbytes(available_mem)})" + ) if not isinstance(self.time_order, int): raise TypeError("`time_order` must be int") diff --git a/tests/test_buffering.py b/tests/test_buffering.py index a7472ae22c..c1196466e3 100644 --- a/tests/test_buffering.py +++ b/tests/test_buffering.py @@ -6,7 +6,7 @@ SubDomain, ConditionalDimension, configuration, switchconfig) from devito.arch.archinfo import AppleArm from devito.ir import FindSymbols, retrieve_iteration_tree -from devito.exceptions import InvalidOperator +from devito.exceptions import CompilationError def test_read_write(): @@ -351,12 +351,8 @@ def define(self, dimensions): eqns = [Eq(u.forward, u + 1, subdomain=s_d0), Eq(u.forward, u.forward + 1, subdomain=s_d1)] - try: + with pytest.raises(CompilationError): Operator(eqns, opt='buffering') - except InvalidOperator: - assert True - except: - assert False @pytest.mark.xfail(reason="Cannot deal with non-overlapping SubDimensions yet")