Skip to content

Commit d363c4a

Browse files
committed
feat: add fmod operator implementation, tests and benchmark
1 parent 93e4d30 commit d363c4a

7 files changed

Lines changed: 174 additions & 0 deletions

File tree

benchmark/test_binary_pointwise_perf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ def get_tflops(self, op, *args, **kwargs):
7676
("bitwise_or", torch.bitwise_or, INT_DTYPES + BOOL_DTYPES),
7777
("div", torch.div, FLOAT_DTYPES + COMPLEX_DTYPES),
7878
("dunder_or", lambda a, b: a | b, INT_DTYPES + BOOL_DTYPES),
79+
# Comparison operations
7980
("eq", torch.eq, FLOAT_DTYPES),
8081
("equal", torch.equal, FLOAT_DTYPES),
8182
("floor_divide", torch.floor_divide, INT_DTYPES),

benchmark/test_fmod_perf.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import pytest
2+
import torch
3+
4+
from benchmark.attri_util import FLOAT_DTYPES
5+
from benchmark.performance_utils import GenericBenchmark, generate_tensor_input
6+
7+
8+
def fmod_input_fn(shape, cur_dtype, device):
9+
inp1 = generate_tensor_input(shape, cur_dtype, device)
10+
inp2 = generate_tensor_input(shape, cur_dtype, device)
11+
inp2 = torch.where(inp2 == 0, torch.ones_like(inp2), inp2)
12+
yield inp1, inp2
13+
14+
15+
def fmod_scalar_input_fn(shape, cur_dtype, device):
16+
inp1 = generate_tensor_input(shape, cur_dtype, device)
17+
yield inp1, 0.5
18+
19+
20+
@pytest.mark.fmod
21+
def test_perf_fmod():
22+
bench = GenericBenchmark(
23+
input_fn=fmod_input_fn,
24+
op_name="fmod",
25+
torch_op=torch.fmod,
26+
dtypes=FLOAT_DTYPES,
27+
)
28+
bench.run()
29+
30+
31+
@pytest.mark.fmod
32+
def test_perf_fmod_scalar():
33+
bench = GenericBenchmark(
34+
input_fn=fmod_scalar_input_fn,
35+
op_name="fmod.Scalar",
36+
torch_op=torch.fmod,
37+
dtypes=FLOAT_DTYPES,
38+
)
39+
bench.run()

src/flag_gems/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,10 @@ def torch_ge(v):
213213
("floor_divide_.Tensor", floor_divide_),
214214
("fmin", fmin),
215215
("fmin.out", fmin_out),
216+
("fmod.Scalar", fmod),
217+
("fmod.Tensor", fmod),
218+
("fmod_.Scalar", fmod_),
219+
("fmod_.Tensor", fmod_),
216220
("full", full),
217221
("full_like", full_like),
218222
("gather", gather),

src/flag_gems/ops/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@
124124
from flag_gems.ops.flip import flip
125125
from flag_gems.ops.floor_ import floor_
126126
from flag_gems.ops.fmin import fmin, fmin_out
127+
from flag_gems.ops.fmod import fmod, fmod_
127128
from flag_gems.ops.full import full
128129
from flag_gems.ops.full_like import full_like
129130
from flag_gems.ops.gather import gather, gather_backward
@@ -476,6 +477,8 @@
476477
"floor_divide_",
477478
"fmin",
478479
"fmin_out",
480+
"fmod",
481+
"fmod_",
479482
"full",
480483
"full_like",
481484
"gather",

src/flag_gems/ops/fmod.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
import triton.language as tl
6+
7+
from flag_gems.utils import pointwise_dynamic
8+
from flag_gems.utils.triton_lang_extension import fmod as _fmod
9+
10+
logger = logging.getLogger(__name__)
11+
12+
13+
@pointwise_dynamic(promotion_methods=[(0, 1, "DEFAULT")])
14+
@triton.jit
15+
def fmod_func(x, y):
16+
# Convert to float32 for computation to avoid libdevice float16/bfloat16 issues
17+
dtype = x.dtype
18+
x_fp32 = x.to(tl.float32)
19+
y_fp32 = y.to(tl.float32)
20+
result = _fmod(x_fp32, y_fp32)
21+
return result.to(dtype)
22+
23+
24+
@pointwise_dynamic(is_tensor=[True, False], promotion_methods=[(0, 1, "DEFAULT")])
25+
@triton.jit
26+
def fmod_func_tensor_scalar(x, y):
27+
# Convert to float32 for computation to avoid libdevice float16/bfloat16 issues
28+
dtype = x.dtype
29+
x_fp32 = x.to(tl.float32)
30+
y_fp32 = y.to(tl.float32)
31+
result = _fmod(x_fp32, y_fp32)
32+
return result.to(dtype)
33+
34+
35+
def fmod(A, B):
36+
logger.debug("GEMS FMOD")
37+
if isinstance(A, torch.Tensor) and isinstance(B, torch.Tensor):
38+
return fmod_func(A, B)
39+
elif isinstance(A, torch.Tensor):
40+
return fmod_func_tensor_scalar(A, B)
41+
else:
42+
# Both scalar - fallback to PyTorch
43+
return torch.fmod(torch.tensor(A), B)
44+
45+
46+
def fmod_(A, B):
47+
logger.debug("GEMS FMOD_")
48+
if isinstance(B, torch.Tensor):
49+
return fmod_func(A, B, out0=A)
50+
else:
51+
return fmod_func_tensor_scalar(A, B, out0=A)

tests/test_binary_pointwise_ops.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2722,3 +2722,5 @@ def test_dunder_ior_scalar(shape, dtype):
27222722
inp1 |= inp2
27232723

27242724
gems_assert_equal(inp1, ref_inp1)
2725+
2726+

tests/test_fmod.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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_close,
11+
to_reference,
12+
)
13+
14+
15+
@pytest.mark.fmod
16+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
17+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
18+
def test_accuracy_fmod(shape, dtype):
19+
inp1 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
20+
inp2 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
21+
inp2 = torch.where(inp2 == 0, torch.ones_like(inp2), inp2)
22+
ref_inp1 = to_reference(inp1, True)
23+
ref_inp2 = to_reference(inp2, True)
24+
ref_out = torch.fmod(ref_inp1, ref_inp2)
25+
with flag_gems.use_gems():
26+
res_out = torch.fmod(inp1, inp2)
27+
gems_assert_close(res_out, ref_out, dtype)
28+
29+
30+
@pytest.mark.fmod
31+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
32+
@pytest.mark.parametrize("scalar", SCALARS)
33+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
34+
def test_accuracy_fmod_tensor_scalar(shape, scalar, dtype):
35+
inp1 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
36+
inp2 = scalar if scalar != 0 else 1.0
37+
ref_inp1 = to_reference(inp1, True)
38+
ref_out = torch.fmod(ref_inp1, inp2)
39+
with flag_gems.use_gems():
40+
res_out = torch.fmod(inp1, inp2)
41+
atol = 1e-3 if abs(scalar) < 0.01 else 1e-4
42+
gems_assert_close(res_out, ref_out, dtype, atol=atol)
43+
44+
45+
@pytest.mark.inplace
46+
@pytest.mark.fmod_
47+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
48+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
49+
def test_accuracy_fmod_(shape, dtype):
50+
inp1 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
51+
inp2 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
52+
inp2 = torch.where(inp2 == 0, torch.ones_like(inp2), inp2)
53+
ref_inp1 = to_reference(inp1.clone(), True)
54+
ref_inp2 = to_reference(inp2, True)
55+
ref_out = ref_inp1.fmod_(ref_inp2)
56+
with flag_gems.use_gems():
57+
res_out = inp1.fmod_(inp2)
58+
gems_assert_close(res_out, ref_out, dtype)
59+
60+
61+
@pytest.mark.inplace
62+
@pytest.mark.fmod_
63+
@pytest.mark.parametrize("shape", POINTWISE_SHAPES)
64+
@pytest.mark.parametrize("scalar", SCALARS)
65+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
66+
def test_accuracy_fmod_tensor_scalar_(shape, scalar, dtype):
67+
inp1 = torch.randn(shape, dtype=dtype, device=flag_gems.device)
68+
inp2 = scalar if scalar != 0 else 1.0
69+
ref_inp1 = to_reference(inp1.clone(), True)
70+
ref_out = ref_inp1.fmod_(inp2)
71+
with flag_gems.use_gems():
72+
res_out = inp1.fmod_(inp2)
73+
atol = 1e-3 if abs(scalar) < 0.01 else 1e-4
74+
gems_assert_close(res_out, ref_out, dtype, atol=atol)

0 commit comments

Comments
 (0)