Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

misc: Revamp exception hierarchy #2500

Merged
merged 11 commits into from
Dec 23, 2024
2 changes: 1 addition & 1 deletion devito/arch/archinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion devito/builtins/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions devito/core/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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']:
Expand Down
54 changes: 44 additions & 10 deletions devito/exceptions.py
Original file line number Diff line number Diff line change
@@ -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.
"""
6 changes: 3 additions & 3 deletions devito/ir/clusters/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
4 changes: 2 additions & 2 deletions devito/ir/iet/visitors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
37 changes: 27 additions & 10 deletions devito/operator/operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -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()

Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
8 changes: 5 additions & 3 deletions devito/passes/clusters/aliases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)

Expand Down
10 changes: 5 additions & 5 deletions devito/passes/clusters/buffering.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
18 changes: 11 additions & 7 deletions devito/passes/iet/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -203,13 +200,16 @@ 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)
args = list(efunc0.parameters)
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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions devito/passes/iet/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,4 +108,6 @@ class Retval(LocalObject, Expr):
error_mapper = {
'Stability': 100,
'KernelLaunch': 200,
'KernelLaunchOutOfResources': 201,
'KernelLaunchUnknown': 202,
}
4 changes: 2 additions & 2 deletions devito/passes/iet/orchestration.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading