Skip to content

Commit 6e69aad

Browse files
[MLIR] find/rfind: skip the scan for empty targets (avoid OOB load)
For an empty target the search length was src_nb + 1, so the loop reached idx == src_nb and the character-position load read one byte past the source (a null pointer for an invalid/null row). Include tgt_empty in the search_len guard so empty targets skip the scan; the result selects still give find("")==0, rfind("")==len and "" in s==True. Adds empty-target coverage, including an empty target on a null masked row (CodeRabbit, mroeschke).
1 parent 18b4798 commit 6e69aad

2 files changed

Lines changed: 83 additions & 2 deletions

File tree

‎python/cudf/cudf/core/udf/mlir_backend/string_lowering_impl.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,8 +232,11 @@ def _lower_find(str_view: ir.Value, target: ir.Value) -> ir.Value:
232232
)
233233
too_long = arith.cmpi(arith.CmpIPredicate.sgt, tgt_nb, src_nb)
234234
# number of candidate start offsets = src_nb - tgt_nb + 1 (0 if target longer)
235+
# Skip the scan entirely for an empty target (handled by the result select
236+
# below); this also avoids the char-position load reading one byte past the
237+
# source when ``search_len`` would otherwise be ``src_nb + 1``.
235238
search_len = arith.select(
236-
too_long,
239+
arith.ori(too_long, tgt_empty),
237240
arith.constant(T.i64(), 0),
238241
arith.addi(arith.subi(src_nb, tgt_nb), one_i64),
239242
)
@@ -280,8 +283,11 @@ def _lower_rfind(str_view: ir.Value, target: ir.Value) -> ir.Value:
280283
arith.CmpIPredicate.eq, tgt_nb, arith.constant(T.i64(), 0)
281284
)
282285
too_long = arith.cmpi(arith.CmpIPredicate.sgt, tgt_nb, src_nb)
286+
# Skip the scan entirely for an empty target (handled by the result select
287+
# below); this also avoids the char-position load reading one byte past the
288+
# source when ``search_len`` would otherwise be ``src_nb + 1``.
283289
search_len = arith.select(
284-
too_long,
290+
arith.ori(too_long, tgt_empty),
285291
arith.constant(T.i64(), 0),
286292
arith.addi(arith.subi(src_nb, tgt_nb), one_i64),
287293
)

‎python/cudf/cudf/tests/private_objects/mlir_backend/test_strings_lowering.py‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -650,3 +650,78 @@ def k(o, ov, s, sv):
650650
for i in range(n):
651651
if valid[i]:
652652
assert bool(out.get()[i]) is ("ab" in strings[i])
653+
654+
655+
def test_string_search_empty_target():
656+
"""Empty-target search matches Python and never over-reads the source."""
657+
strings = ["abc", "", "h\u00e9llo"]
658+
arr, _k = _make_mlir_string_array(strings)
659+
n = len(strings)
660+
finds = cp.zeros(n, dtype=np.int32)
661+
rfinds = cp.zeros(n, dtype=np.int32)
662+
counts = cp.zeros(n, dtype=np.int32)
663+
has = cp.zeros(n, dtype=np.bool_)
664+
665+
@cuda.jit(
666+
types.void(
667+
types.int32[::1],
668+
types.int32[::1],
669+
types.int32[::1],
670+
types.boolean[::1],
671+
types.CPointer(mlir_string),
672+
),
673+
extensions=[mlir_string_arg_handler],
674+
)
675+
def k(fo, ro, co, ho, s):
676+
i = cuda.grid(1)
677+
if i < fo.size:
678+
fo[i] = s[i].find("")
679+
ro[i] = s[i].rfind("")
680+
co[i] = s[i].count("")
681+
ho[i] = "" in s[i]
682+
683+
with MLIRNumbaCudaConfig():
684+
k[1, n](finds, rfinds, counts, has, arr)
685+
cuda.synchronize()
686+
assert finds.get().tolist() == [s.find("") for s in strings]
687+
assert rfinds.get().tolist() == [s.rfind("") for s in strings]
688+
assert counts.get().tolist() == [s.count("") for s in strings]
689+
assert has.get().tolist() == ["" in s for s in strings]
690+
691+
692+
def test_masked_string_empty_target_null_row():
693+
"""Empty-target search on a null row must not over-read its payload.
694+
695+
A null row carries a null ``data`` pointer; with the empty target the scan
696+
must be skipped entirely (otherwise the char-position load would dereference
697+
the null pointer). Validity still propagates.
698+
"""
699+
strings = ["abc", None]
700+
valid = [True, False]
701+
arr, _k = _make_mlir_string_array(strings)
702+
n = len(strings)
703+
out = cp.zeros(n, dtype=np.int32)
704+
out_valid = cp.zeros(n, dtype=np.bool_)
705+
706+
@cuda.jit(
707+
types.void(
708+
types.int32[::1],
709+
types.boolean[::1],
710+
types.CPointer(mlir_string),
711+
types.boolean[::1],
712+
),
713+
extensions=[mlir_string_arg_handler],
714+
)
715+
def k(o, ov, s, sv):
716+
i = cuda.grid(1)
717+
if i < o.size:
718+
r = Masked(s[i], sv[i]).find("")
719+
o[i] = r.value
720+
ov[i] = r.valid
721+
722+
with MLIRNumbaCudaConfig():
723+
k[1, n](out, out_valid, arr, cp.array(valid, dtype=np.bool_))
724+
cuda.synchronize()
725+
assert out_valid.get().tolist() == valid
726+
# value only meaningful where valid; "abc".find("") == 0
727+
assert int(out.get()[0]) == 0

0 commit comments

Comments
 (0)