Skip to content

Commit 35e7cd6

Browse files
authored
[MLIR] MaskedType: NA handling (#22885)
Part of the MLIR UDF backend stack. Depends on #22766 (plumbing). Adds NA handling for MaskedType: NAType + cudf.NA typeof, MaskedType / NAType unify, `m is NA` / `m is not NA`, and the NA / scalar / Masked->Masked casts used for branch unification. Unification covers expressions in the target code like the following where `x` is `MaskedType`. ``` if x is cudf.NA: return x else: return 42 ``` 42 is a scalar here so the unification code kicks in and types the overall expression as returning MaskedType, and upcasts the 42 to Masked(42, True)
1 parent fb23b0d commit 35e7cd6

5 files changed

Lines changed: 360 additions & 29 deletions

File tree

python/cudf/cudf/core/udf/mlir_backend/masked_lowering.py

Lines changed: 93 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
# SPDX-License-Identifier: Apache-2.0
33
from __future__ import annotations
44

5+
import operator
6+
from functools import partial
57
from typing import TYPE_CHECKING
68

79
from numba_cuda_mlir import types
810
from numba_cuda_mlir._mlir import ir as mlir_ir
9-
from numba_cuda_mlir._mlir.dialects import llvm
10-
from numba_cuda_mlir.extending import lowering_registry
11+
from numba_cuda_mlir._mlir.dialects import arith, llvm
12+
from numba_cuda_mlir.extending import lower_cast, lowering_registry
1113
from numba_cuda_mlir.lowering_utilities import convert
1214
from numba_cuda_mlir.models import PrimitiveModel, register_model
1315

1416
from cudf.core.udf.api import Masked
15-
from cudf.core.udf.mlir_backend.masked_typing import MaskedType
17+
from cudf.core.udf.mlir_backend.masked_typing import (
18+
MaskedType,
19+
NAType,
20+
na_type,
21+
)
1622

1723
if TYPE_CHECKING:
1824
from numba_cuda_mlir.mlir_lowering import MLIRLower
@@ -62,6 +68,21 @@ def __init__(
6268
super().__init__(data_model_manager, masked_type, struct_type)
6369

6470

71+
def _extract_masked_value_valid(struct_val, value_mlir_ty, valid_ty):
72+
"""Pull the ``(value, valid)`` SSA values out of a ``Masked`` struct."""
73+
v = llvm.extractvalue(
74+
res=value_mlir_ty,
75+
container=struct_val,
76+
position=mlir_ir.DenseI64ArrayAttr.get([0]),
77+
)
78+
valid = llvm.extractvalue(
79+
res=valid_ty,
80+
container=struct_val,
81+
position=mlir_ir.DenseI64ArrayAttr.get([1]),
82+
)
83+
return v, valid
84+
85+
6586
def _lower_masked_constructor(
6687
builder: MLIRLower, target: Var, args: list[Var], kwargs: list
6788
) -> None:
@@ -105,6 +126,63 @@ def _lower_masked_getattr(
105126
builder.store_var(target, convert(field_value, target_mlir_ty))
106127

107128

129+
# ``cast(NA -> Masked)`` and ``cast(scalar -> Masked)``. Both build a Masked
130+
# struct for the target's value type; they differ only in the payload (NA has
131+
# none, so use undef) and the validity bit (NA -> invalid, scalar -> valid).
132+
# Triggered by branch unification, e.g. ``return x if cond else cudf.NA`` or
133+
# ``return 5``.
134+
def _cast_to_masked(context, builder, from_ty, to_ty, val):
135+
value_mlir_ty = builder.get_mlir_type(to_ty.value_type)
136+
if isinstance(from_ty, NAType):
137+
value = llvm.UndefOp(value_mlir_ty)
138+
valid = 0
139+
else:
140+
value = convert(val, value_mlir_ty)
141+
valid = 1
142+
valid_const = arith.constant(
143+
result=builder.get_mlir_type(types.boolean), value=valid
144+
)
145+
return _pack_masked(builder, to_ty, value, valid_const)
146+
147+
148+
# ``cast(Masked -> Masked)``: branch unification across different
149+
# inner widths (e.g. one branch returns Masked(int32), another
150+
# returns Masked(float64); Numba unifies to Masked(float64)).
151+
# Promote the payload, preserve the validity bit.
152+
def _cast_masked_to_masked(context, builder, from_ty, to_ty, val):
153+
if from_ty.value_type == to_ty.value_type:
154+
return val
155+
st = llvm.StructType(val.type)
156+
m_val, m_valid = _extract_masked_value_valid(val, st.body[0], st.body[1])
157+
value_mlir_ty = builder.get_mlir_type(to_ty.value_type)
158+
casted = convert(m_val, value_mlir_ty)
159+
return _pack_masked(builder, to_ty, casted, m_valid)
160+
161+
162+
# ``is``/``is not`` against NA are registered for both operand orders
163+
# (``m is NA`` and ``NA is m``), so find the MaskedType operand rather than
164+
# assuming which position it is in.
165+
def _masked_operand(builder, args):
166+
"""Return the ``MaskedType`` operand of a ``Masked``/``NA`` comparison."""
167+
for arg in args:
168+
if isinstance(builder.get_numba_type(arg.name), MaskedType):
169+
return arg
170+
raise TypeError("expected a MaskedType operand")
171+
172+
173+
# ``is``/``is not`` against NA both reduce to the validity bit: ``m is NA`` ->
174+
# ``not m.valid`` and ``m is not NA`` -> ``m.valid``. Registered for both
175+
# operators (and operand orders) via partials below.
176+
def _lower_masked_na_compare(builder, target, args, kwargs, *, is_null):
177+
m = builder.load_var(_masked_operand(builder, args))
178+
st = llvm.StructType(m.type)
179+
_, valid = _extract_masked_value_valid(m, st.body[0], st.body[1])
180+
if is_null:
181+
one = arith.constant(valid.type, 1)
182+
valid = arith.xori(valid, one)
183+
builder.store_var(target, valid)
184+
185+
108186
def _register() -> None:
109187
"""Register the data model and lowerings with ``numba_cuda_mlir``.
110188
@@ -123,5 +201,17 @@ def _register() -> None:
123201

124202
lowering_registry.lower_getattr_generic(MaskedType)(_lower_masked_getattr)
125203

204+
lower_cast(na_type, MaskedType)(_cast_to_masked)
205+
for _scalar_cls in (types.Integer, types.Float, types.Boolean):
206+
lower_cast(_scalar_cls, MaskedType)(_cast_to_masked)
207+
lower_cast(MaskedType, MaskedType)(_cast_masked_to_masked)
208+
209+
is_na = partial(_lower_masked_na_compare, is_null=True)
210+
is_not_na = partial(_lower_masked_na_compare, is_null=False)
211+
lower(operator.is_, MaskedType, NAType)(is_na)
212+
lower(operator.is_, NAType, MaskedType)(is_na)
213+
lower(operator.is_not, MaskedType, NAType)(is_not_na)
214+
lower(operator.is_not, NAType, MaskedType)(is_not_na)
215+
126216

127217
_register()

python/cudf/cudf/core/udf/mlir_backend/masked_typing.py

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
22
# SPDX-License-Identifier: Apache-2.0
33
from __future__ import annotations
44

5-
from numba_cuda_mlir import types
5+
import operator
6+
7+
from numba_cuda_mlir import models, types
68
from numba_cuda_mlir.extending import typing_registry
9+
from numba_cuda_mlir.models import register_model
710
from numba_cuda_mlir.numba_cuda import types as nb_types
11+
from numba_cuda_mlir.numba_cuda.extending import typeof_impl
812
from numba_cuda_mlir.numba_cuda.types.misc import unliteral
913
from numba_cuda_mlir.numba_cuda.typing.templates import (
14+
AbstractTemplate,
1015
AttributeTemplate,
1116
ConcreteTemplate,
1217
)
1318
from numba_cuda_mlir.typing import signature as nb_signature
1419

20+
from cudf.core.missing import NA
1521
from cudf.core.udf.api import Masked
1622

1723
_SUPPORTED_MASKED_VALUE_TYPE_CLASSES = (
@@ -49,6 +55,55 @@ def __hash__(self) -> int:
4955
# parameter ``value_type`` matches, so numba can cache them by repr.
5056
return hash(repr(self))
5157

58+
def unify(self, context, other):
59+
"""Pick a common type when branches return different shapes.
60+
61+
``return x if cond else cudf.NA`` unifies ``MaskedType`` and
62+
``NAType``; ``return x if cond else 5`` unifies ``MaskedType``
63+
and a scalar; two branches returning different ``Masked``
64+
widths unify their inner value types. Returning ``None``
65+
signals "no unifier" and lets numba fall through.
66+
See https://numba.pydata.org/numba-doc/dev/user/troubleshoot.html#my-code-has-a-type-unification-problem
67+
"""
68+
if isinstance(other, NAType):
69+
return self
70+
other_value_type = (
71+
other.value_type if isinstance(other, MaskedType) else other
72+
)
73+
unified = context.unify_pairs(self.value_type, other_value_type)
74+
return MaskedType(unified) if unified is not None else None
75+
76+
77+
class NAType(types.Type):
78+
"""Type for ``cudf.NA`` -- the missing-value sentinel that can flow
79+
through a UDF branch and unify into a ``MaskedType``.
80+
"""
81+
82+
def __init__(self):
83+
super().__init__(name="NA")
84+
85+
def unify(self, context, other):
86+
# See https://numba.pydata.org/numba-doc/dev/user/troubleshoot.html#my-code-has-a-type-unification-problem
87+
# NA + Masked is delegated to MaskedType.unify (see above) so we
88+
# only need to handle NA + (NA | scalar) here.
89+
if isinstance(other, MaskedType):
90+
return None
91+
if isinstance(other, NAType):
92+
return self
93+
return MaskedType(other)
94+
95+
96+
na_type = NAType()
97+
98+
99+
@typeof_impl.register(type(NA))
100+
def _typeof_na(val, c):
101+
return na_type
102+
103+
104+
# NAType has no payload; OpaqueModel is the right data model.
105+
register_model(NAType)(models.OpaqueModel)
106+
52107

53108
# ``Masked(value, valid)`` constructor: produces a ``Masked(value_ty)``.
54109
class MaskedConstructor(ConcreteTemplate):
@@ -71,12 +126,27 @@ def resolve_valid(self, typ: MaskedType) -> types.Type:
71126
return types.boolean
72127

73128

129+
# ``is``/``is not`` between a ``MaskedType`` and ``NA`` (either operand order)
130+
# both type to boolean; the is/is-not distinction is handled in lowering, so a
131+
# single template serves both operators.
132+
class MaskedNAComparison(AbstractTemplate):
133+
def generic(self, args, kws):
134+
if len(args) != 2 or kws:
135+
return None
136+
lhs, rhs = args
137+
if {type(lhs), type(rhs)} == {MaskedType, NAType}:
138+
return nb_signature(types.boolean, lhs, rhs)
139+
return None
140+
141+
74142
def _register() -> None:
75143
"""Register typing for ``Masked`` and ``MaskedType`` attributes with
76144
``numba_cuda_mlir``. Called once at module import.
77145
"""
78146
typing_registry.register_global(Masked, types.Function(MaskedConstructor))
79147
typing_registry.register_attr(MaskedTypeAttrs)
148+
typing_registry.register_global(operator.is_)(MaskedNAComparison)
149+
typing_registry.register_global(operator.is_not)(MaskedNAComparison)
80150

81151

82152
_register()

python/cudf/cudf/tests/private_objects/mlir_backend/conftest.py

Lines changed: 0 additions & 25 deletions
This file was deleted.

0 commit comments

Comments
 (0)