Skip to content

Commit a8a4050

Browse files
committed
Add clip operator implementation, tests and benchmark
1 parent a267340 commit a8a4050

6 files changed

Lines changed: 173 additions & 18 deletions

File tree

benchmark/test_clip_perf.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
import torch
3+
4+
from benchmark.attri_util import FLOAT_DTYPES
5+
from benchmark.conftest import BenchLevel, Config
6+
from benchmark.performance_utils import GenericBenchmark, generate_tensor_input
7+
8+
9+
def clip_input_fn(shape, cur_dtype, device):
10+
inp = generate_tensor_input(shape, cur_dtype, device)
11+
yield inp, -0.5, 0.5
12+
if Config.bench_level == BenchLevel.COMPREHENSIVE:
13+
yield inp, None, 0.5
14+
yield inp, -0.5, None
15+
16+
17+
@pytest.mark.clip
18+
def test_clip():
19+
bench = GenericBenchmark(
20+
input_fn=clip_input_fn,
21+
op_name="clip",
22+
torch_op=torch.clip,
23+
dtypes=FLOAT_DTYPES,
24+
)
25+
bench.run()
26+
27+
28+
@pytest.mark.clip_
29+
def test_clip_inplace():
30+
bench = GenericBenchmark(
31+
input_fn=clip_input_fn,
32+
op_name="clip_",
33+
torch_op=torch.clip_,
34+
dtypes=FLOAT_DTYPES,
35+
is_inplace=True,
36+
)
37+
bench.run()

benchmark/test_generic_pointwise_perf.py

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,31 @@ def test_generic_pointwise_benchmark(op_name, torch_op, input_fn, dtypes):
161161
bench.run()
162162

163163

164-
@pytest.mark.clamp_
165-
def test_clamp_inplace():
166-
bench = GenericBenchmark(
167-
input_fn=clamp_input_fn,
168-
op_name="clamp_",
169-
torch_op=torch.clamp_,
170-
dtypes=FLOAT_DTYPES,
171-
is_inplace=True,
172-
)
173-
bench.run()
174-
175-
176-
@pytest.mark.clamp_min_
177-
def test_clamp_min_inplace():
164+
@pytest.mark.parametrize(
165+
"op_name, torch_op, input_fn, dtypes",
166+
[
167+
pytest.param(
168+
"clamp_",
169+
torch.clamp_,
170+
clamp_input_fn,
171+
FLOAT_DTYPES,
172+
marks=pytest.mark.clamp_,
173+
),
174+
pytest.param(
175+
"clamp_min_",
176+
torch.clamp_min_,
177+
clamp_min_input_fn,
178+
FLOAT_DTYPES,
179+
marks=pytest.mark.clamp_min_,
180+
),
181+
],
182+
)
183+
def test_generic_inplace_pointwise_benchmark(op_name, torch_op, input_fn, dtypes):
178184
bench = GenericBenchmark(
179-
input_fn=clamp_min_input_fn,
180-
op_name="clamp_min_",
181-
torch_op=torch.clamp_min_,
182-
dtypes=FLOAT_DTYPES,
185+
input_fn=input_fn,
186+
op_name=op_name,
187+
torch_op=torch_op,
188+
dtypes=dtypes,
183189
is_inplace=True,
184190
)
185191
bench.run()

src/flag_gems/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ def torch_ge(v):
129129
("clamp_", clamp_),
130130
("clamp_.Tensor", clamp_tensor_),
131131
("clamp_min_", clamp_min_),
132+
("clip", clip),
133+
("clip_", clip_),
132134
("conj_physical", conj_physical),
133135
("constant_pad_nd", constant_pad_nd),
134136
# ("contiguous", contiguous),

src/flag_gems/ops/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
clamp_tensor,
7272
clamp_tensor_,
7373
)
74+
from flag_gems.ops.clip import clip, clip_
7475
from flag_gems.ops.conj_physical import conj_physical
7576
from flag_gems.ops.contiguous import contiguous
7677
from flag_gems.ops.conv1d import conv1d
@@ -410,6 +411,8 @@
410411
"clamp_min_",
411412
"clamp_tensor",
412413
"clamp_tensor_",
414+
"clip",
415+
"clip_",
413416
"constant_pad_nd",
414417
"contiguous",
415418
"conv1d",

src/flag_gems/ops/clip.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import logging
2+
3+
import triton
4+
import triton.language as tl
5+
6+
from flag_gems.utils import pointwise_dynamic
7+
8+
logger = logging.getLogger(__name__)
9+
10+
11+
@pointwise_dynamic(
12+
is_tensor=[True, False, False], promotion_methods=[(0, 1, 2, "DEFAULT")]
13+
)
14+
@triton.jit
15+
def clip_func(x, mini, maxi):
16+
return tl.minimum(maxi, tl.maximum(mini, x.to(tl.float32)))
17+
18+
19+
@pointwise_dynamic(is_tensor=[True, False], promotion_methods=[(0, 1, "DEFAULT")])
20+
@triton.jit
21+
def clip_func_min(x, mini):
22+
return tl.maximum(mini, x.to(tl.float32))
23+
24+
25+
@pointwise_dynamic(is_tensor=[True, False], promotion_methods=[(0, 1, "DEFAULT")])
26+
@triton.jit
27+
def clip_func_max(x, maxi):
28+
return tl.minimum(maxi, x.to(tl.float32))
29+
30+
31+
def clip(A, mini=None, maxi=None):
32+
logger.debug("GEMS CLIP")
33+
if mini is None and maxi is None:
34+
raise ValueError("At least one of mini or maxi must not be None")
35+
elif mini is None:
36+
return clip_func_max(A, maxi)
37+
elif maxi is None:
38+
return clip_func_min(A, mini)
39+
else:
40+
return clip_func(A, mini, maxi)
41+
42+
43+
def clip_(A, mini=None, maxi=None):
44+
logger.debug("GEMS CLIP_")
45+
if mini is None and maxi is None:
46+
raise ValueError("At least one of mini or maxi must not be None")
47+
elif mini is None:
48+
return clip_func_max(A, maxi, out0=A)
49+
elif maxi is None:
50+
return clip_func_min(A, mini, out0=A)
51+
else:
52+
return clip_func(A, mini, maxi, out0=A)

tests/test_clip.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import pytest
2+
import torch
3+
4+
import flag_gems
5+
6+
from .accuracy_utils import (
7+
FLOAT_DTYPES,
8+
POINTWISE_SHAPES,
9+
SCALARS,
10+
gems_assert_equal,
11+
to_reference,
12+
)
13+
14+
15+
@pytest.mark.clip
16+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
17+
@pytest.mark.parametrize("maxi", SCALARS)
18+
@pytest.mark.parametrize("mini", SCALARS)
19+
@pytest.mark.parametrize("isnone", [None, "max", "min"])
20+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
21+
def test_accuracy_clip(shape, maxi, mini, isnone, dtype):
22+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
23+
if isnone == "min":
24+
mini = None
25+
elif isnone == "max":
26+
maxi = None
27+
ref_inp = to_reference(inp)
28+
29+
ref_out = torch.clip(ref_inp, min=mini, max=maxi)
30+
with flag_gems.use_gems():
31+
res_out = torch.clip(inp, min=mini, max=maxi)
32+
33+
gems_assert_equal(res_out, ref_out)
34+
35+
36+
@pytest.mark.inplace
37+
@pytest.mark.clip_
38+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
39+
@pytest.mark.parametrize("maxi", SCALARS)
40+
@pytest.mark.parametrize("mini", SCALARS)
41+
@pytest.mark.parametrize("isnone", [None, "max", "min"])
42+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
43+
def test_accuracy_clip_(shape, maxi, mini, isnone, dtype):
44+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
45+
if isnone == "min":
46+
mini = None
47+
elif isnone == "max":
48+
maxi = None
49+
ref_inp = to_reference(inp.clone())
50+
51+
ref_out = torch.clip_(ref_inp, min=mini, max=maxi)
52+
with flag_gems.use_gems():
53+
res_out = torch.clip_(inp, min=mini, max=maxi)
54+
55+
gems_assert_equal(res_out, ref_out)

0 commit comments

Comments
 (0)