diff --git a/grudge/op.py b/grudge/op.py
index b3c402ca9c3f3c739f4e80378d5a284ad769f0d9..014bd670cee1d0e516d2626e0bc0b033e0ca7b58 100644
--- a/grudge/op.py
+++ b/grudge/op.py
@@ -24,7 +24,10 @@
 .. autofunction:: cross_rank_trace_pairs
 """
 
-__copyright__ = "Copyright (C) 2021 Andreas Kloeckner"
+__copyright__ = """
+Copyright (C) 2021 Andreas Kloeckner
+Copyright (C) 2021 University of Illinois Board of Trustees
+"""
 
 __license__ = """
 Permission is hereby granted, free of charge, to any person obtaining a copy
@@ -48,13 +51,18 @@ THE SOFTWARE.
 
 
 from numbers import Number
-from pytools import memoize_on_first_arg
-
-import numpy as np  # noqa
-from pytools.obj_array import obj_array_vectorize, make_obj_array
-import pyopencl.array as cla  # noqa
-from grudge import sym, bind
-
+from pytools import (
+    memoize_in,
+    memoize_on_first_arg,
+    # keyed_memoize_in
+)
+from pytools.obj_array import obj_array_vectorize
+from meshmode.array_context import (
+    FirstAxisIsElementsTag, make_loopy_program
+)
+from meshmode.dof_array import DOFArray
+
+import numpy as np
 import grudge.dof_desc as dof_desc
 
 from meshmode.mesh import BTAG_ALL, BTAG_NONE, BTAG_PARTITION  # noqa
@@ -63,12 +71,26 @@ from meshmode.dof_array import freeze, flatten, unflatten
 from grudge.symbolic.primitives import TracePair
 
 
-# def interp(dcoll, src, tgt, vec):
-#     from warnings import warn
-#     warn("using 'interp' is deprecated, use 'project' instead.",
-#             DeprecationWarning, stacklevel=2)
-#
-#     return dcoll.project(src, tgt, vec)
+# {{{ tags
+
+class HasElementwiseMatvecTag(FirstAxisIsElementsTag):
+    pass
+
+
+class MassOperatorTag(HasElementwiseMatvecTag):
+    pass
+
+# }}}
+
+
+# {{{ Interpolation and projection
+
+def interp(dcoll, src, tgt, vec):
+    from warnings import warn
+    warn("using 'interp' is deprecated, use 'project' instead.",
+            DeprecationWarning, stacklevel=2)
+
+    return project(dcoll, src, tgt, vec)
 
 
 def project(dcoll, src, tgt, vec):
@@ -94,6 +116,7 @@ def project(dcoll, src, tgt, vec):
 
     return dcoll.connection_from_dds(src, tgt)(vec)
 
+# }}}
 
 # {{{ geometric properties
 
@@ -117,22 +140,56 @@ def normal(dcoll, dd):
     :arg dd: a :class:`~grudge.dof_desc.DOFDesc` as the surface discretization.
     :returns: an object array of :class:`~meshmode.dof_array.DOFArray`.
     """
+    from grudge.geometry import normal
     surface_discr = dcoll.discr_from_dd(dd)
     actx = surface_discr._setup_actx
-    return freeze(
-            bind(dcoll,
-                sym.normal(dd, surface_discr.ambient_dim, surface_discr.dim),
-                local_only=True)
-            (array_context=actx))
+    return actx.freeze(normal(actx, dcoll, dd))
 
 # }}}
 
 
-# {{{ derivatives
-
-@memoize_on_first_arg
-def _bound_grad(dcoll):
-    return bind(dcoll, sym.nabla(dcoll.dim) * sym.Variable("u"), local_only=True)
+# {{{ Derivative operators
+
+def reference_derivative_matrices(actx, element_group):
+    # @keyed_memoize_in(
+    #     actx, reference_derivative_matrices,
+    #     lambda grp: grp.discretization_key())
+    def get_ref_derivative_mats(grp):
+        from meshmode.discretization.poly_element import diff_matrices
+        return tuple(
+            actx.freeze(actx.from_numpy(dfmat))
+            for dfmat in diff_matrices(grp)
+        )
+    return get_ref_derivative_mats(element_group)
+
+
+def _compute_local_gradient(dcoll, vec, xyz_axis):
+    from grudge.geometry import \
+        inverse_surface_metric_derivative
+
+    discr = dcoll.discr_from_dd(dof_desc.DD_VOLUME)
+    actx = vec.array_context
+
+    # FIXME: Each individual term comes out as (result,)
+    inverse_jac_t = [
+        inverse_surface_metric_derivative(
+            actx, dcoll, rst_axis, xyz_axis
+        )[0] for rst_axis in range(dcoll.dim)
+    ]
+
+    data = []
+    for grp, vec_i in zip(discr.groups, vec):
+        data.append(sum(
+            actx.einsum(
+                "ij,ej,ej->ei",
+                reference_derivative_matrices(actx, grp)[rst_axis],
+                vec_i,
+                inverse_jac_t[rst_axis],
+                arg_names=("ref_diff_mat", "vec", "inv_jac_t"),
+                tagged=(HasElementwiseMatvecTag(),)
+            ) for rst_axis in range(dcoll.dim))
+        )
+    return DOFArray(actx, data=tuple(data))
 
 
 def local_grad(dcoll, vec):
@@ -142,13 +199,11 @@ def local_grad(dcoll, vec):
     :arg vec: a :class:`~meshmode.dof_array.DOFArray`
     :returns: an object array of :class:`~meshmode.dof_array.DOFArray`\ s
     """
-    return _bound_grad(dcoll)(u=vec)
-
-
-@memoize_on_first_arg
-def _bound_d_dx(dcoll, xyz_axis):
-    return bind(dcoll, sym.nabla(dcoll.dim)[xyz_axis] * sym.Variable("u"),
-            local_only=True)
+    from pytools.obj_array import make_obj_array
+    return make_obj_array(
+        [_compute_local_gradient(dcoll, vec, xyz_axis)
+         for xyz_axis in range(dcoll.dim)]
+    )
 
 
 def local_d_dx(dcoll, xyz_axis, vec):
@@ -160,7 +215,7 @@ def local_d_dx(dcoll, xyz_axis, vec):
     :arg vec: a :class:`~meshmode.dof_array.DOFArray`
     :returns: a :class:`~meshmode.dof_array.DOFArray`\ s
     """
-    return _bound_d_dx(dcoll, xyz_axis)(u=vec)
+    return _compute_local_gradient(dcoll, vec, xyz_axis)
 
 
 def _div_helper(dcoll, diff_func, vecs):
@@ -169,16 +224,18 @@ def _div_helper(dcoll, diff_func, vecs):
     assert vecs.dtype == object
 
     if vecs.shape[-1] != dcoll.ambient_dim:
-        raise ValueError("last dimension of *vecs* argument must match "
-                "ambient dimension")
+        raise ValueError(
+            "last dimension of *vecs* argument must match "
+            "ambient dimension"
+        )
 
     if len(vecs.shape) == 1:
         return sum(diff_func(i, vec_i) for i, vec_i in enumerate(vecs))
     else:
         result = np.zeros(vecs.shape[:-1], dtype=object)
         for idx in np.ndindex(vecs.shape[:-1]):
-            result[idx] = sum(
-                    diff_func(i, vec_i) for i, vec_i in enumerate(vecs[idx]))
+            result[idx] = \
+                sum(diff_func(i, vec_i) for i, vec_i in enumerate(vecs[idx]))
         return result
 
 
@@ -193,16 +250,91 @@ def local_div(dcoll, vecs):
     :returns: a :class:`~meshmode.dof_array.DOFArray`
     """
 
-    return _div_helper(dcoll,
-            lambda i, subvec: local_d_dx(dcoll, i, subvec),
-            vecs)
+    return _div_helper(
+        dcoll,
+        lambda i, subvec: local_d_dx(dcoll, i, subvec),
+        vecs
+    )
 
+# }}}
 
-@memoize_on_first_arg
-def _bound_weak_grad(dcoll, dd):
-    return bind(dcoll,
-            sym.stiffness_t(dcoll.dim, dd_in=dd) * sym.Variable("u", dd),
-            local_only=True)
+
+# {{{ Weak derivative operators
+
+def reference_stiffness_transpose_matrix(actx, out_element_group, in_element_group):
+    # FIXME: Think about how to compose this better with existing functions
+    # @keyed_memoize_in(
+    #     actx, reference_stiffness_transpose_matrix,
+    #     lambda out_grp, in_grp: (out_grp.discretization_key(),
+    #                              in_grp.discretization_key()))
+    def get_ref_stiffness_transpose_mat(out_grp, in_grp):
+        if in_grp == out_grp:
+            mmat = reference_mass_matrix(actx, out_grp, in_grp)
+            return [dmat.T.dot(mmat.T)
+                    for dmat in reference_derivative_matrices(actx, in_grp)]
+
+        from modepy import vandermonde
+        basis = out_grp.basis_obj()
+        vand = vandermonde(basis.functions, out_grp.unit_nodes)
+        grad_vand = vandermonde(basis.gradients, in_grp.unit_nodes)
+        vand_inv_t = np.linalg.inv(vand).T
+
+        if not isinstance(grad_vand, tuple):
+            # NOTE: special case for 1d
+            grad_vand = (grad_vand,)
+
+        weights = in_grp.quadrature_rule().weights
+        return actx.freeze(
+            actx.from_numpy(
+                np.einsum(
+                    "c,bz,acz->abc",
+                    weights,
+                    vand_inv_t,
+                    grad_vand
+                )
+            )
+        )
+    return get_ref_stiffness_transpose_mat(out_element_group,
+                                           in_element_group)
+
+
+def _apply_stiffness_transpose_operator(dcoll, dd_out, dd_in, vec, xyz_axis):
+    from grudge.geometry import \
+        inverse_surface_metric_derivative, area_element
+
+    in_discr = dcoll.discr_from_dd(dd_in)
+    out_discr = dcoll.discr_from_dd(dd_out)
+
+    actx = vec.array_context
+    area_elements = area_element(actx, dcoll, dd=dd_in)
+    # FIXME: Each individual term comes out as (result,)
+    inverse_jac_t = [
+        inverse_surface_metric_derivative(
+            actx, dcoll, rst_axis, xyz_axis
+        )[0] for rst_axis in range(dcoll.dim)
+    ]
+    data = []
+    for out_grp, in_grp, vec_i, ae_i in zip(out_discr.groups,
+                                            in_discr.groups,
+                                            vec,
+                                            area_elements):
+        ref_stiffness_t = reference_stiffness_transpose_matrix(
+            actx,
+            out_element_group=out_grp,
+            in_element_group=in_grp
+        )
+        data.append(sum(
+            actx.einsum(
+                "ej,ij,ej,ej->ei",
+                inverse_jac_t[rst_axis],
+                ref_stiffness_t[rst_axis],
+                vec_i,
+                ae_i,
+                arg_names=("inv_jac_t", "ref_stiffT_mat", "vec", "jac"),
+                tagged=(HasElementwiseMatvecTag(),)
+            ) for rst_axis in range(dcoll.dim))
+        )
+    return DOFArray(actx, data=tuple(data))
 
 
 def weak_local_grad(dcoll, *args):
@@ -224,15 +356,14 @@ def weak_local_grad(dcoll, *args):
     else:
         raise TypeError("invalid number of arguments")
 
-    return _bound_weak_grad(dcoll, dd)(u=vec)
-
+    from pytools.obj_array import make_obj_array
 
-@memoize_on_first_arg
-def _bound_weak_d_dx(dcoll, dd, xyz_axis):
-    return bind(dcoll,
-            sym.stiffness_t(dcoll.dim, dd_in=dd)[xyz_axis]
-            * sym.Variable("u", dd),
-            local_only=True)
+    return make_obj_array(
+        [_apply_stiffness_transpose_operator(dcoll,
+                                             dof_desc.DD_VOLUME,
+                                             dd, vec, xyz_axis)
+         for xyz_axis in range(dcoll.dim)]
+    )
 
 
 def weak_local_d_dx(dcoll, *args):
@@ -254,7 +385,9 @@ def weak_local_d_dx(dcoll, *args):
     else:
         raise TypeError("invalid number of arguments")
 
-    return _bound_weak_d_dx(dcoll, dd, xyz_axis)(u=vec)
+    return _apply_stiffness_transpose_operator(
+        dcoll, dof_desc.DD_VOLUME, dd, vec, xyz_axis
+    )
 
 
 def weak_local_div(dcoll, *args):
@@ -279,25 +412,87 @@ def weak_local_div(dcoll, *args):
     else:
         raise TypeError("invalid number of arguments")
 
-    return _div_helper(dcoll,
-            lambda i, subvec: weak_local_d_dx(dcoll, dd, i, subvec),
-            vecs)
+    return _div_helper(
+        dcoll,
+        lambda i, subvec: weak_local_d_dx(dcoll, dd, i, subvec),
+        vecs
+    )
 
 # }}}
 
 
-# {{{ mass-like
+# {{{ Mass operator
+
+def reference_mass_matrix(actx, out_element_group, in_element_group):
+    # @keyed_memoize_in(
+    #     actx, reference_mass_matrix,
+    #     lambda out_grp, in_grp: (out_grp.discretization_key(),
+    #                              in_grp.discretization_key()))
+    def get_ref_mass_mat(out_grp, in_grp):
+        if out_grp == in_grp:
+            from meshmode.discretization.poly_element import mass_matrix
+
+            return actx.freeze(
+                actx.from_numpy(
+                    np.asarray(
+                        mass_matrix(out_grp),
+                        order="C"
+                    )
+                )
+            )
+
+        from modepy import vandermonde
+        basis = out_grp.basis_obj()
+        vand = vandermonde(basis.functions, out_grp.unit_nodes)
+        o_vand = vandermonde(basis.functions, in_grp.unit_nodes)
+        vand_inv_t = np.linalg.inv(vand).T
+
+        weights = in_grp.quadrature_rule().weights
+        return actx.freeze(
+            actx.from_numpy(
+                np.asarray(
+                    np.einsum("j,ik,jk->ij", weights, vand_inv_t, o_vand),
+                    order="C"
+                )
+            )
+        )
+
+    return get_ref_mass_mat(out_element_group, in_element_group)
 
-@memoize_on_first_arg
-def _bound_mass(dcoll, dd):
-    return bind(dcoll, sym.MassOperator(dd_in=dd)(sym.Variable("u", dd)),
-            local_only=True)
+
+def _apply_mass_operator(dcoll, dd_out, dd_in, vec):
+    from grudge.geometry import area_element
+
+    in_discr = dcoll.discr_from_dd(dd_in)
+    out_discr = dcoll.discr_from_dd(dd_out)
+
+    actx = vec.array_context
+    area_elements = area_element(actx, dcoll, dd=dd_in)
+    return DOFArray(
+        actx,
+        tuple(
+            actx.einsum(
+                "ij,ej,ej->ei",
+                reference_mass_matrix(
+                    actx,
+                    out_element_group=out_grp,
+                    in_element_group=in_grp
+                ),
+                ae_i,
+                vec_i,
+                arg_names=("mass_mat", "jac_det", "vec"),
+                tagged=(MassOperatorTag(),))
+
+            for in_grp, out_grp, ae_i, vec_i in zip(
+                    in_discr.groups, out_discr.groups, area_elements, vec)
+        )
+    )
 
 
 def mass(dcoll, *args):
     if len(args) == 1:
         vec, = args
-        dd = dof_desc.DOFDesc("vol", dof_desc.DISCR_TAG_BASE)
+        dd = dof_desc.DOFDesc("vol", dof_desc.QTAG_NONE)
     elif len(args) == 2:
         dd, vec = args
     else:
@@ -305,35 +500,249 @@ def mass(dcoll, *args):
 
     if isinstance(vec, np.ndarray):
         return obj_array_vectorize(
-                lambda el: mass(dcoll, dd, el), vec)
+            lambda el: mass(dcoll, dd, el), vec
+        )
 
-    return _bound_mass(dcoll, dd)(u=vec)
+    return _apply_mass_operator(dcoll, dof_desc.DD_VOLUME, dd, vec)
 
+# }}}
 
-@memoize_on_first_arg
-def _bound_inverse_mass(dcoll):
-    return bind(dcoll, sym.InverseMassOperator()(sym.Variable("u")),
-            local_only=True)
+
+# {{{ Mass inverse operator
+
+def reference_inverse_mass_matrix(actx, element_group):
+    # @keyed_memoize_in(
+    #     actx, reference_inverse_mass_matrix,
+    #     lambda grp: grp.discretization_key())
+    def get_ref_inv_mass_mat(grp):
+        from modepy import inverse_mass_matrix
+        basis = grp.basis_obj()
+
+        return actx.freeze(
+            actx.from_numpy(
+                np.asarray(
+                    inverse_mass_matrix(basis.functions, grp.unit_nodes),
+                    order="C"
+                )
+            )
+        )
+
+    return get_ref_inv_mass_mat(element_group)
+
+
+def _apply_inverse_mass_operator(dcoll, dd_out, dd_in, vec):
+    from grudge.geometry import area_element
+
+    if dd_out != dd_in:
+        raise ValueError(
+            "Cannot compute inverse of a mass matrix mapping "
+            "between different element groups; inverse is not "
+            "guaranteed to be well-defined"
+        )
+    discr = dcoll.discr_from_dd(dd_in)
+    use_wadg = not all(grp.is_affine for grp in discr.groups)
+
+    actx = vec.array_context
+    inv_area_elements = 1./area_element(actx, dcoll, dd=dd_in)
+    if use_wadg:
+        # FIXME: Think of how to compose existing functions here...
+        # NOTE: Rewritten for readability/debuggability
+        grps = discr.groups
+        data = []
+        for grp, jac_inv, x in zip(grps, inv_area_elements, vec):
+            ref_mass = reference_mass_matrix(actx,
+                                             out_element_group=grp,
+                                             in_element_group=grp)
+            ref_mass_inv = reference_inverse_mass_matrix(actx,
+                                                         element_group=grp)
+            data.append(
+                # Based on https://arxiv.org/pdf/1608.03836.pdf
+                # true_Minv ~ ref_Minv * ref_M * (1/jac_det) * ref_Minv
+                actx.einsum("ik,km,em,mj,ej->ei",
+                            ref_mass_inv, ref_mass, jac_inv, ref_mass_inv, x,
+                            tagged=(MassOperatorTag(),))
+            )
+        return DOFArray(actx, data=tuple(data))
+    else:
+        return DOFArray(
+            actx,
+            tuple(
+                actx.einsum("ij,ej,ej->ei",
+                            reference_inverse_mass_matrix(
+                                actx,
+                                element_group=grp
+                            ),
+                            iae_i,
+                            vec_i,
+                            arg_names=("mass_inv_mat", "jac_det_inv", "vec"),
+                            tagged=(MassOperatorTag(),))
+
+                for grp, iae_i, vec_i in zip(discr.groups,
+                                             inv_area_elements, vec)
+            )
+        )
 
 
 def inverse_mass(dcoll, vec):
     if isinstance(vec, np.ndarray):
         return obj_array_vectorize(
-                lambda el: inverse_mass(dcoll, el), vec)
+            lambda el: inverse_mass(dcoll, el), vec
+        )
+
+    return _apply_inverse_mass_operator(
+        dcoll, dof_desc.DD_VOLUME, dof_desc.DD_VOLUME, vec
+    )
 
-    return _bound_inverse_mass(dcoll)(u=vec)
+# }}}
 
 
-@memoize_on_first_arg
-def _bound_face_mass(dcoll, dd):
-    u = sym.Variable("u", dd=dd)
-    return bind(dcoll, sym.FaceMassOperator(dd_in=dd)(u), local_only=True)
+# {{{ Face mass operator
+
+def reference_face_mass_matrix(actx, face_element_group, vol_element_group, dtype):
+
+    # @keyed_memoize_in(
+    #     actx, reference_mass_matrix,
+    #     lambda face_grp, vol_grp: (face_grp.discretization_key(),
+    #                                vol_grp.discretization_key()))
+    def get_ref_face_mass_mat(face_grp, vol_grp):
+        nfaces = vol_grp.mesh_el_group.nfaces
+        assert (face_grp.nelements
+                == nfaces * vol_grp.nelements)
+
+        matrix = np.empty(
+            (vol_grp.nunit_dofs,
+            nfaces,
+            face_grp.nunit_dofs),
+            dtype=dtype
+        )
+
+        import modepy as mp
+        from meshmode.discretization import ElementGroupWithBasis
+        from meshmode.discretization.poly_element import \
+            QuadratureSimplexElementGroup
+
+        n = vol_grp.order
+        m = face_grp.order
+        vol_basis = vol_grp.basis_obj()
+        faces = mp.faces_for_shape(vol_grp.shape)
+
+        for iface, face in enumerate(faces):
+            # If the face group is defined on a higher-order
+            # quadrature grid, use the underlying quadrature rule
+            if isinstance(face_grp, QuadratureSimplexElementGroup):
+                face_quadrature = face_grp.quadrature_rule()
+                if face_quadrature.exact_to < m:
+                    raise ValueError(
+                        "The face quadrature rule is only exact for polynomials "
+                        f"of total degree {face_quadrature.exact_to}. Please "
+                        "ensure a quadrature rule is used that is at least "
+                        f"exact for degree {m}."
+                    )
+            else:
+                # NOTE: This handles the general case where
+                # volume and surface quadrature rules may have different
+                # integration orders
+                face_quadrature = mp.quadrature_for_space(
+                    mp.space_for_shape(face, 2*max(n, m)),
+                    face
+                )
+
+            # If the group has a nodal basis and is unisolvent,
+            # we use the basis on the face to compute the face mass matrix
+            if (isinstance(face_grp, ElementGroupWithBasis)
+                    and face_grp.space.space_dim
+                    == face_grp.nunit_dofs):
+
+                face_basis = face_grp.basis_obj()
+
+                # Sanity check for face quadrature accuracy. Not integrating
+                # degree N + M polynomials here is asking for a bad time.
+                if face_quadrature.exact_to < m + n:
+                    raise ValueError(
+                        "The face quadrature rule is only exact for polynomials "
+                        f"of total degree {face_quadrature.exact_to}. Please "
+                        "ensure a quadrature rule is used that is at least "
+                        f"exact for degree {n+m}."
+                    )
+
+                matrix[:, iface, :] = mp.nodal_mass_matrix_for_face(
+                    face, face_quadrature,
+                    face_basis.functions, vol_basis.functions,
+                    vol_grp.unit_nodes,
+                    face_grp.unit_nodes,
+                )
+            else:
+                # Otherwise, we use a routine that is purely quadrature-based
+                # (no need for explicit face basis functions)
+                matrix[:, iface, :] = mp.nodal_quad_mass_matrix_for_face(
+                    face,
+                    face_quadrature,
+                    vol_basis.functions,
+                    vol_grp.unit_nodes,
+                )
+
+        return actx.freeze(actx.from_numpy(matrix))
+
+    return get_ref_face_mass_mat(face_element_group, vol_element_group)
+
+
+def _apply_face_mass_operator(dcoll, dd, vec):
+    from grudge.geometry import area_element
+
+    volm_discr = dcoll.discr_from_dd(dof_desc.DD_VOLUME)
+    face_discr = dcoll.discr_from_dd(dd)
+    dtype = vec.entry_dtype
+    actx = vec.array_context
+    surf_area_elements = area_element(actx, dcoll, dd=dd)
+
+    @memoize_in(actx, (_apply_face_mass_operator, "face_mass_knl"))
+    def prg():
+        return make_loopy_program(
+            """{[iel,idof,f,j]:
+                0<=iel<nelements and
+                0<=f<nfaces and
+                0<=idof<nvol_nodes and
+                0<=j<nface_nodes}""",
+            """
+            result[iel,idof] = sum(f, sum(j, mat[idof, f, j]       \
+                                             * jac_surf[f, iel, j] \
+                                             * vec[f, iel, j]))
+            """,
+            name="face_mass"
+        )
+
+    result = volm_discr.empty(actx, dtype=dtype)
+    assert len(face_discr.groups) == len(volm_discr.groups)
+
+    for afgrp, volgrp in zip(face_discr.groups, volm_discr.groups):
+
+        nfaces = volgrp.mesh_el_group.nfaces
+        matrix = reference_face_mass_matrix(
+            actx,
+            face_element_group=afgrp,
+            vol_element_group=volgrp,
+            dtype=dtype
+        )
+        input_view = result[afgrp.index].reshape(
+            nfaces, volgrp.nelements, afgrp.nunit_dofs
+        )
+        jac_surf = surf_area_elements[afgrp.index].reshape(
+            nfaces, volgrp.nelements, afgrp.nunit_dofs
+        )
+        actx.call_loopy(
+            prg(),
+            mat=matrix,
+            result=result[volgrp.index],
+            jac_surf=jac_surf,
+            vec=input_view
+        )
+    return result
 
 
 def face_mass(dcoll, *args):
     if len(args) == 1:
         vec, = args
-        dd = dof_desc.DOFDesc("all_faces", dof_desc.DISCR_TAG_BASE)
+        dd = dof_desc.DOFDesc("all_faces", dof_desc.QTAG_NONE)
     elif len(args) == 2:
         dd, vec = args
     else:
@@ -343,7 +752,7 @@ def face_mass(dcoll, *args):
         return obj_array_vectorize(
                 lambda el: face_mass(dcoll, dd, el), vec)
 
-    return _bound_face_mass(dcoll, dd)(u=vec)
+    return _apply_face_mass_operator(dcoll, dd, vec)
 
 # }}}