diff --git a/grudge/discretization.py b/grudge/discretization.py
index 300b5fbc362b4d84ff940a72efd0d686d99dc135..6a7a4fb453e8ff0ae60295eb3602ee80416c266b 100644
--- a/grudge/discretization.py
+++ b/grudge/discretization.py
@@ -83,11 +83,13 @@ class Discretization(object):
     def mesh(self):
         return self.volume_discr.mesh
 
-    def empty(self, queue=None, dtype=None, extra_dims=None):
-        return self.volume_discr.empty(queue, dtype, extra_dims=None)
+    def empty(self, queue=None, dtype=None, extra_dims=None, allocator=None):
+        return self.volume_discr.empty(queue, dtype, extra_dims=None,
+                allocator=allocator)
 
-    def zeros(self, queue, dtype=None, extra_dims=None):
-        return self.volume_discr.zeros(queue, dtype, extra_dims=None)
+    def zeros(self, queue, dtype=None, extra_dims=None, allocator=None):
+        return self.volume_discr.zeros(queue, dtype, extra_dims=None,
+                allocator=allocator)
 
     def is_volume_where(self, where):
         from grudge import sym
diff --git a/grudge/execution.py b/grudge/execution.py
index 4f0293751d79eb91d0dc8837f71140c92b5841f2..2cf24b316cbfdf80b1859a2f00ff9123d213822e 100644
--- a/grudge/execution.py
+++ b/grudge/execution.py
@@ -24,6 +24,11 @@ THE SOFTWARE.
 
 import six
 import numpy as np
+import loopy as lp
+import pyopencl as cl
+import pyopencl.array  # noqa
+from pytools import memoize_in
+
 import grudge.symbolic.mappers as mappers
 from grudge import sym
 
@@ -45,17 +50,17 @@ class ExecutionMapper(mappers.Evaluator,
     # {{{ expression mappings -------------------------------------------------
 
     def map_ones(self, expr):
-        # FIXME
         if expr.quadrature_tag is not None:
+            # FIXME
             raise NotImplementedError("ones on quad. grids")
 
-        result = self.discr.empty(self.queue)
+        result = self.discr.empty(self.queue, allocator=self.bound_op.allocator)
         result.fill(1)
         return result
 
     def map_node_coordinate_component(self, expr):
-        # FIXME
         if expr.quadrature_tag is not None:
+            # FIXME
             raise NotImplementedError("node coordinate components on quad. grids")
 
         if self.discr.is_volume_where(expr.where):
@@ -84,32 +89,25 @@ class ExecutionMapper(mappers.Evaluator,
         return func(*[self.rec(p) for p in expr.parameters])
 
     def map_nodal_sum(self, op, field_expr):
-        return np.sum(self.rec(field_expr))
+        return cl.array.sum(self.rec(field_expr))
 
     def map_nodal_max(self, op, field_expr):
-        return np.max(self.rec(field_expr))
+        return cl.array.max(self.rec(field_expr))
 
     def map_nodal_min(self, op, field_expr):
-        return np.min(self.rec(field_expr))
+        return cl.array.min(self.rec(field_expr))
 
     def map_if(self, expr):
         bool_crit = self.rec(expr.condition)
+
         then = self.rec(expr.then)
         else_ = self.rec(expr.else_)
 
-        true_indices = np.nonzero(bool_crit)
-        false_indices = np.nonzero(~bool_crit)
+        result = cl.array.empty_like(then, queue=self.queue,
+                allocator=self.bound_op.allocator)
+        cl.array.if_positive(bool_crit, then, else_, out=result,
+                queue=self.queue)
 
-        result = self.discr.volume_empty(
-                kind=self.discr.compute_kind)
-
-        if isinstance(then, np.ndarray):
-            then = then[true_indices]
-        if isinstance(else_, np.ndarray):
-            else_ = else_[false_indices]
-
-        result[true_indices] = then
-        result[false_indices] = else_
         return result
 
     def map_ref_diff_base(self, op, field_expr):
@@ -123,28 +121,43 @@ class ExecutionMapper(mappers.Evaluator,
         if is_zero(field):
             return 0
 
-        for eg in self.discr.element_groups:
+        @memoize_in(self.bound_op, "elwise_linear_knl")
+        def knl():
+            knl = lp.make_kernel(
+                """{[k,i,j]:
+                    0<=k<nelements and
+                    0<=i,j<ndiscr_nodes}""",
+                "result[k,i] = sum(j, diff_mat[i, j] * vec[k, j])",
+                default_offset=lp.auto, name="diff")
+
+            knl = lp.split_iname(knl, "i", 16, inner_tag="l.0")
+            return lp.tag_inames(knl, dict(k="g.0"))
+
+        discr = self.discr.volume_discr
+
+        # FIXME: This shouldn't really assume that it's dealing with a volume
+        # input. What about quadrature? What about boundaries?
+        result = discr.empty(
+                queue=self.queue,
+                dtype=field.dtype, allocator=self.bound_op.allocator)
+
+        for grp in discr.groups:
+            cache_key = grp, op, field.dtype
             try:
-                matrix, coeffs = self.bound_op.elwise_linear_cache[eg, op, field.dtype]
+                matrix = self.bound_op.elwise_linear_cache[cache_key]
             except KeyError:
-                matrix = np.asarray(op.matrix(eg), dtype=field.dtype)
-                coeffs = op.coefficients(eg)
-                self.elwise_linear_cache[eg, op, field.dtype] = matrix, coeffs
+                matrix = (
+                        cl.array.to_device(
+                            self.queue,
+                            np.asarray(op.matrix(grp), dtype=field.dtype))
+                        .with_queue(None))
 
-            from grudge._internal import (
-                    perform_elwise_scaled_operator,
-                    perform_elwise_operator)
+                self.bound_op.elwise_linear_cache[cache_key] = matrix
 
-            if coeffs is None:
-                perform_elwise_operator(eg.ranges, eg.ranges,
-                        matrix, field, out)
-            else:
-                perform_elwise_scaled_operator(eg.ranges, eg.ranges,
-                        coeffs, matrix, field, out)
+            knl()(self.queue, diff_mat=matrix, result=grp.view(result),
+                    vec=grp.view(field))
 
-        out = self.discr.volume_zeros()
-        self.bound_op.do_elementwise_linear(op, field, out)
-        return out
+        return result
 
     def map_ref_quad_mass(self, op, field_expr):
         field = self.rec(field_expr)
@@ -401,6 +414,10 @@ class ExecutionMapper(mappers.Evaluator,
             # FIXME
             raise NotImplementedError()
 
+        # FIXME: There's no real reason why differentiation is special,
+        # execution-wise.
+        # This should be unified with map_elementwise_linear, which should
+        # be extended to support batching.
         if self.discr.is_volume_where(repr_op.where):
             discr = self.discr.volume_discr
         else:
@@ -422,11 +439,12 @@ class ExecutionMapper(mappers.Evaluator,
 # {{{ bound operator
 
 class BoundOperator(object):
-    def __init__(self, discr, code, debug_flags):
+    def __init__(self, discr, code, debug_flags, allocator=None):
         self.discr = discr
         self.code = code
         self.elwise_linear_cache = {}
         self.debug_flags = debug_flags
+        self.allocator = allocator
 
     def __call__(self, queue, **context):
         import pyopencl.array as cl_array
@@ -527,7 +545,7 @@ def process_sym_operator(sym_operator, post_bind_mapper=None,
 
 
 def bind(discr, sym_operator, post_bind_mapper=lambda x: x, type_hints={},
-        debug_flags=set()):
+        debug_flags=set(), allocator=None):
     # from grudge.symbolic.mappers import QuadratureUpsamplerRemover
     # sym_operator = QuadratureUpsamplerRemover(self.quad_min_degrees)(
     #         sym_operator)
@@ -549,9 +567,11 @@ def bind(discr, sym_operator, post_bind_mapper=lambda x: x, type_hints={},
             type_hints=type_hints)
 
     from grudge.symbolic.compiler import OperatorCompiler
-    code = OperatorCompiler()(sym_operator, type_hints)
+    code = OperatorCompiler()(sym_operator,
+            type_hints=type_hints)
 
-    bound_op = BoundOperator(discr, code, type_hints)
+    bound_op = BoundOperator(discr, code,
+            debug_flags=debug_flags, allocator=allocator)
 
     if "dump_op_code" in debug_flags:
         from grudge.tools import open_unique_debug_file
diff --git a/grudge/symbolic/compiler.py b/grudge/symbolic/compiler.py
index 466475229bdc9caf189431c9ace585dd2a6a5de9..55c5fa6d598c40c7bed6a5751e02b7944add9c62 100644
--- a/grudge/symbolic/compiler.py
+++ b/grudge/symbolic/compiler.py
@@ -594,7 +594,7 @@ class Code(object):
             self.last_schedule = None
             self.static_schedule_attempts -= 1
 
-        from grudge.tools import with_object_array_or_scalar
+        from pytools.obj_array import with_object_array_or_scalar
         return with_object_array_or_scalar(exec_mapper, self.result)
 
     # }}}
diff --git a/grudge/symbolic/operators.py b/grudge/symbolic/operators.py
index c63d2f8cdece59e1a17aad20e6f4bbd001ce9efd..c5375c4f68eb0dab3e4f0aad1fa840a835e2ee64 100644
--- a/grudge/symbolic/operators.py
+++ b/grudge/symbolic/operators.py
@@ -226,9 +226,6 @@ class ElementwiseLinearOperator(Operator):
     def matrix(self, element_group):
         raise NotImplementedError
 
-    def coefficients(self, element_group):
-        return None
-
     mapper_method = intern("map_elementwise_linear")
 
 
@@ -348,31 +345,15 @@ class VandermondeOperator(ElementwiseLinearOperator):
 
 # {{{ mass operators
 
-class MassOperatorBase(ElementwiseLinearOperator):
+class MassOperatorBase(Operator):
     pass
 
 
 class MassOperator(MassOperatorBase):
-    @staticmethod
-    def matrix(element_group):
-        return element_group.mass_matrix
-
-    @staticmethod
-    def coefficients(element_group):
-        return element_group.jacobians
-
     mapper_method = intern("map_mass")
 
 
 class InverseMassOperator(MassOperatorBase):
-    @staticmethod
-    def matrix(element_group):
-        return element_group.inverse_mass_matrix
-
-    @staticmethod
-    def coefficients(element_group):
-        return element_group.inverse_jacobians
-
     mapper_method = intern("map_inverse_mass")
 
 
@@ -423,11 +404,10 @@ class ReferenceMassOperatorBase(MassOperatorBase):
 class ReferenceMassOperator(ReferenceMassOperatorBase):
     @staticmethod
     def matrix(element_group):
-        return element_group.mass_matrix
-
-    @staticmethod
-    def coefficients(element_group):
-        return None
+        import modepy as mp
+        return mp.mass_matrix(
+                element_group.basis(),
+                element_group.unit_nodes)
 
     mapper_method = intern("map_ref_mass")
 
@@ -435,11 +415,10 @@ class ReferenceMassOperator(ReferenceMassOperatorBase):
 class ReferenceInverseMassOperator(ReferenceMassOperatorBase):
     @staticmethod
     def matrix(element_group):
-        return element_group.inverse_mass_matrix
-
-    @staticmethod
-    def coefficients(element_group):
-        return None
+        import modepy as mp
+        return mp.inverse_mass_matrix(
+                element_group.basis(),
+                element_group.unit_nodes)
 
     mapper_method = intern("map_ref_inverse_mass")
 
@@ -782,7 +761,7 @@ def stiffness_t(dim):
 
 
 def integral(arg):
-    import grudge.symbolic as sym
+    from grudge import sym
     return sym.NodalSum()(sym.MassOperator()(sym.Ones())*arg)
 
 
diff --git a/test/test_grudge.py b/test/test_grudge.py
index bc871da57335fd83a5bcac9d6e2cf4868cc1d2b2..3754f49f4b596b335004ab3c4574a9e2619b302c 100644
--- a/test/test_grudge.py
+++ b/test/test_grudge.py
@@ -85,6 +85,82 @@ def test_inverse_metric(ctx_getter, dim):
             assert err < 1e-12, (i, j, err)
 
 
+def test_1d_mass_mat_trig(ctx_getter):
+    """Check the integral of some trig functions on an interval using the mass
+    matrix
+    """
+
+    cl_ctx = cl.create_some_context()
+    queue = cl.CommandQueue(cl_ctx)
+
+    from meshmode.mesh.generation import generate_regular_rect_mesh
+
+    mesh = generate_regular_rect_mesh(a=(-4*np.pi,), b=(9*np.pi,),
+            n=(17,), order=1)
+
+    discr = Discretization(cl_ctx, mesh, order=8)
+
+    x = sym.nodes(1)
+    f = bind(discr, sym.cos(x[0])**2)(queue)
+    ones = bind(discr, sym.Ones())(queue)
+
+    mass_op = bind(discr, sym.MassOperator()(sym.Field("f")))
+
+    num_integral_1 = np.dot(ones.get(), mass_op(queue, f=f).get())
+    num_integral_2 = np.dot(f.get(), mass_op(queue, f=ones).get())
+    num_integral_3 = bind(discr, sym.integral(sym.Field("f")))(queue, f=f).get()
+
+    true_integral = 13*np.pi/2
+    err_1 = abs(num_integral_1-true_integral)
+    err_2 = abs(num_integral_2-true_integral)
+    err_3 = abs(num_integral_3-true_integral)
+
+    assert err_1 < 1e-10
+    assert err_2 < 1e-10
+    assert err_3 < 1e-10
+
+
+@pytest.mark.parametrize("dim", [1, 2, 3])
+def test_tri_diff_mat(ctx_getter, dim, order=4):
+    """Check differentiation matrix along the coordinate axes on a disk
+
+    Uses sines as the function to differentiate.
+    """
+
+    cl_ctx = cl.create_some_context()
+    queue = cl.CommandQueue(cl_ctx)
+
+    from meshmode.mesh.generation import generate_regular_rect_mesh
+
+    from pytools.convergence import EOCRecorder
+    axis_eoc_recs = [EOCRecorder() for axis in range(dim)]
+
+    for n in [10, 20]:
+        mesh = generate_regular_rect_mesh(a=(-0.5,)*dim, b=(0.5,)*dim,
+                n=(n,)*dim, order=4)
+
+        discr = Discretization(cl_ctx, mesh, order=4)
+        nabla = sym.nabla(dim)
+
+        for axis in range(dim):
+            x = sym.nodes(dim)
+
+            f = bind(discr, sym.sin(3*x[axis]))(queue)
+            df = bind(discr, 3*sym.cos(3*x[axis]))(queue)
+
+            sym_op = nabla[axis](sym.Field("f"))
+            bound_op = bind(discr, sym_op)
+            df_num = bound_op(queue, f=f)
+
+            linf_error = la.norm((df_num-df).get(), np.Inf)
+            axis_eoc_recs[axis].add_data_point(1/n, linf_error)
+
+    for axis, eoc_rec in enumerate(axis_eoc_recs):
+        print(axis)
+        print(eoc_rec)
+        assert eoc_rec.order_estimate() >= order
+
+
 # You can test individual routines by typing
 # $ python test_layer_pot.py 'test_routine()'