Skip to content

Commit 036fadc

Browse files
Schopenhauer-loves-Hegelfactnnclaudebin913
authored andcommitted
feat: add feature_dropout operator with tests and benchmark (flagos-ai#1732)
Co-authored-by: factnn <1050552884@qq.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: bin913 <842884726@qq.com>
1 parent 878a3c2 commit 036fadc

5 files changed

Lines changed: 343 additions & 0 deletions

File tree

benchmark/test_feature_dropout.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import pytest
2+
import torch
3+
4+
from . import base, consts, utils
5+
6+
7+
def _input_fn(shape, dtype, device):
8+
inp = utils.generate_tensor_input(shape, dtype, device)
9+
yield inp, 0.5, True
10+
11+
12+
@pytest.mark.feature_dropout
13+
def test_feature_dropout():
14+
bench = base.GenericBenchmarkExcluse1D(
15+
input_fn=_input_fn,
16+
op_name="feature_dropout",
17+
torch_op=torch.feature_dropout,
18+
dtypes=consts.FLOAT_DTYPES,
19+
)
20+
bench.run()
21+
22+
23+
@pytest.mark.feature_dropout_
24+
def test_feature_dropout_():
25+
bench = base.GenericBenchmarkExcluse1D(
26+
input_fn=_input_fn,
27+
op_name="feature_dropout_",
28+
torch_op=torch.feature_dropout_,
29+
dtypes=consts.FLOAT_DTYPES,
30+
)
31+
bench.run()

src/flag_gems/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ def torch_ge(v):
213213
("expm1_", expm1_),
214214
("expm1.out", expm1_out),
215215
("exponential_", exponential_),
216+
("feature_dropout", feature_dropout),
217+
("feature_dropout_", feature_dropout_),
216218
("eye", eye),
217219
("eye.m", eye_m),
218220
("fill.Scalar", fill_scalar),

src/flag_gems/ops/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@
119119
from flag_gems.ops.exponential_ import exponential_
120120
from flag_gems.ops.eye import eye
121121
from flag_gems.ops.eye_m import eye_m
122+
from flag_gems.ops.feature_dropout import feature_dropout, feature_dropout_
122123
from flag_gems.ops.fill import (
123124
fill_scalar,
124125
fill_scalar_,
@@ -505,6 +506,8 @@
505506
"exponential_",
506507
"eye",
507508
"eye_m",
509+
"feature_dropout",
510+
"feature_dropout_",
508511
"fill_scalar",
509512
"fill_scalar_",
510513
"fill_scalar_out",
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import logging
2+
3+
import torch
4+
import triton
5+
import triton.language as tl
6+
7+
from flag_gems.runtime import torch_device_fn
8+
from flag_gems.utils.random_utils import (
9+
philox_backend_seed_offset,
10+
uint_to_uniform_float,
11+
)
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
@triton.jit(do_not_specialize=["p", "philox_seed", "philox_offset"])
17+
def generate_feature_mask_kernel(
18+
MASK,
19+
N, # batch size
20+
C, # number of channels
21+
p,
22+
scale,
23+
philox_seed,
24+
philox_offset,
25+
BLOCK_N: tl.constexpr,
26+
BLOCK_C: tl.constexpr,
27+
):
28+
"""
29+
Generate a feature dropout mask of shape (N, C).
30+
Each element is either 0 (dropped) or scale (kept).
31+
Each (n, c) pair gets its own random value.
32+
"""
33+
philox_seed = philox_seed.to(tl.int64)
34+
philox_offset = philox_offset.to(tl.int64)
35+
36+
pid_n = tl.program_id(0)
37+
pid_c = tl.program_id(1)
38+
39+
n_offset = pid_n * BLOCK_N + tl.arange(0, BLOCK_N)
40+
c_offset = pid_c * BLOCK_C + tl.arange(0, BLOCK_C)
41+
42+
n_mask = n_offset < N
43+
c_mask = c_offset < C
44+
45+
# Compute flat indices for random number generation
46+
# flat_idx = n * C + c
47+
flat_idx = n_offset[:, None] * C + c_offset[None, :]
48+
49+
# Generate random numbers using philox
50+
c0 = (philox_offset & 0xFFFFFFFF).to(tl.uint32)
51+
c1 = ((philox_offset >> 32) & 0xFFFFFFFF).to(tl.uint32)
52+
i4 = flat_idx.to(tl.uint32)
53+
c0 = c0 + i4
54+
_O = c0 * 0
55+
r0, _, _, _ = tl.philox(philox_seed, c0, c1, _O, _O)
56+
rand_vals = uint_to_uniform_float(r0)
57+
58+
# Create mask: scale if rand > p (keep), 0 if rand <= p (drop)
59+
mask_vals = tl.where(rand_vals > p, scale, 0.0)
60+
61+
# Store mask
62+
mask_offsets = n_offset[:, None] * C + c_offset[None, :]
63+
mask_mask = n_mask[:, None] & c_mask[None, :]
64+
tl.store(MASK + mask_offsets, mask_vals, mask=mask_mask)
65+
66+
67+
@triton.jit
68+
def apply_feature_mask_kernel(
69+
X,
70+
Y,
71+
MASK,
72+
numel,
73+
N, # batch size
74+
C, # channels
75+
spatial_size, # H * W or D1 * D2 * ...
76+
BLOCK: tl.constexpr,
77+
):
78+
"""
79+
Apply feature mask to input tensor.
80+
Input shape: (N, C, ...) flattened to (numel,)
81+
Mask shape: (N, C)
82+
83+
For element at flat index i:
84+
- For contiguous (N, C, H, W) layout: i = n * (C * spatial) + c * spatial + spatial_idx
85+
- n = i // (C * spatial_size)
86+
- c = (i // spatial_size) % C
87+
- mask_idx = n * C + c
88+
"""
89+
pid = tl.program_id(0)
90+
offset = pid * BLOCK + tl.arange(0, BLOCK)
91+
mask = offset < numel
92+
93+
# Compute batch and channel index for each element
94+
channel_spatial_size = C * spatial_size
95+
n_idx = offset // channel_spatial_size
96+
c_idx = (offset % channel_spatial_size) // spatial_size
97+
98+
# Compute mask index: n * C + c
99+
mask_idx = n_idx * C + c_idx
100+
101+
# Load input and mask
102+
x = tl.load(X + offset, mask=mask, other=0.0)
103+
m = tl.load(MASK + mask_idx, mask=mask, other=0.0)
104+
105+
# Apply mask
106+
y = x * m
107+
108+
tl.store(Y + offset, y, mask=mask)
109+
110+
111+
def feature_dropout(input, p, train=True):
112+
"""
113+
Applies feature dropout to the input tensor.
114+
115+
Randomly zeroes out entire channels of the input tensor with probability p.
116+
Each batch element has its own independent channel mask.
117+
118+
Args:
119+
input: Input tensor of shape (N, C, ...) where N is batch size, C is channels
120+
p: Probability of a channel to be zeroed. Default: 0.5
121+
train: If True, applies dropout. If False, returns input unchanged.
122+
123+
Returns:
124+
Output tensor of same shape as input
125+
"""
126+
logger.debug("GEMS FEATURE_DROPOUT")
127+
128+
if not train or p == 0:
129+
return input.clone()
130+
131+
if p == 1:
132+
return torch.zeros_like(input)
133+
134+
if input.ndim < 2:
135+
raise RuntimeError(
136+
"Feature dropout requires at least 2 dimensions in the input"
137+
)
138+
139+
assert 0.0 < p < 1.0, "p must be in (0, 1)"
140+
141+
device = input.device
142+
input = input.contiguous()
143+
out = torch.empty_like(input)
144+
145+
# Get dimensions
146+
batch_size = input.shape[0]
147+
num_channels = input.shape[1]
148+
spatial_size = 1
149+
for i in range(2, input.ndim):
150+
spatial_size *= input.shape[i]
151+
152+
N = batch_size
153+
C = num_channels
154+
numel = input.numel()
155+
scale = 1.0 / (1.0 - p)
156+
157+
# Create mask tensor of shape (N, C)
158+
mask = torch.empty(N, C, device=device, dtype=torch.float32)
159+
160+
# Generate mask
161+
BLOCK_N = min(triton.next_power_of_2(N), 64)
162+
BLOCK_C = min(triton.next_power_of_2(C), 64)
163+
grid_mask = (triton.cdiv(N, BLOCK_N), triton.cdiv(C, BLOCK_C))
164+
165+
# Need N * C random numbers
166+
increment = triton.cdiv(N * C, 4) * 4
167+
with torch_device_fn.device(device):
168+
philox_seed, philox_offset = philox_backend_seed_offset(increment)
169+
generate_feature_mask_kernel[grid_mask](
170+
mask, N, C, p, scale, philox_seed, philox_offset, BLOCK_N, BLOCK_C
171+
)
172+
173+
# Apply mask to input
174+
BLOCK = 1024
175+
grid_apply = (triton.cdiv(numel, BLOCK),)
176+
177+
with torch_device_fn.device(device):
178+
apply_feature_mask_kernel[grid_apply](
179+
input, out, mask, numel, N, C, spatial_size, BLOCK
180+
)
181+
182+
return out
183+
184+
185+
def feature_dropout_(input, p, train=True):
186+
"""
187+
In-place version of feature_dropout.
188+
"""
189+
logger.debug("GEMS FEATURE_DROPOUT_")
190+
if not train or p == 0:
191+
return input
192+
if p == 1:
193+
input.zero_()
194+
return input
195+
out = feature_dropout(input, p, train)
196+
input.copy_(out)
197+
return input

tests/test_feature_dropout.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import pytest
2+
import torch
3+
4+
import flag_gems
5+
6+
from .accuracy_utils import (
7+
FLOAT_DTYPES,
8+
gems_assert_close,
9+
gems_assert_equal,
10+
to_reference,
11+
)
12+
from .conftest import QUICK_MODE
13+
14+
FEATURE_DROPOUT_SHAPES = (
15+
[(2, 8, 4, 4)]
16+
if QUICK_MODE
17+
else [(2, 3), (4, 8, 16), (2, 16, 8, 8), (2, 32, 4, 4, 4)]
18+
)
19+
20+
21+
@pytest.mark.feature_dropout
22+
@pytest.mark.parametrize("shape", FEATURE_DROPOUT_SHAPES)
23+
@pytest.mark.parametrize("p", [0.3, 0.5, 0.7])
24+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
25+
def test_feature_dropout(shape, p, dtype):
26+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
27+
with flag_gems.use_gems():
28+
res_out = torch.feature_dropout(inp, p, True)
29+
assert res_out.shape == inp.shape
30+
batch_size, num_channels = shape[0], shape[1]
31+
scale = 1.0 / (1.0 - p)
32+
inp_reshaped = inp.view(batch_size, num_channels, -1)
33+
out_reshaped = res_out.view(batch_size, num_channels, -1)
34+
for b in range(batch_size):
35+
for c in range(num_channels):
36+
channel_out = out_reshaped[b, c]
37+
channel_inp = inp_reshaped[b, c]
38+
if not torch.all(channel_out == 0).item():
39+
assert torch.allclose(
40+
channel_out, channel_inp * scale, rtol=1e-4, atol=1e-5
41+
)
42+
out_by_channel = res_out.view(batch_size, num_channels, -1)
43+
dropped = sum(
44+
1
45+
for b in range(batch_size)
46+
for c in range(num_channels)
47+
if torch.all(out_by_channel[b, c] == 0)
48+
)
49+
total = batch_size * num_channels
50+
tolerance = max(0.3, 2.0 / (total**0.5)) if total < 50 else 0.2
51+
assert abs(dropped / total - p) < tolerance
52+
53+
54+
@pytest.mark.feature_dropout
55+
@pytest.mark.parametrize("shape", FEATURE_DROPOUT_SHAPES)
56+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
57+
def test_feature_dropout_no_train(shape, dtype):
58+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
59+
ref_inp = to_reference(inp)
60+
ref = torch.feature_dropout(ref_inp, 0.5, False)
61+
with flag_gems.use_gems():
62+
res_out = torch.feature_dropout(inp, 0.5, False)
63+
gems_assert_close(res_out, ref, dtype)
64+
65+
66+
@pytest.mark.feature_dropout
67+
@pytest.mark.parametrize("shape", FEATURE_DROPOUT_SHAPES)
68+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
69+
def test_feature_dropout_p_zero(shape, dtype):
70+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
71+
ref_inp = to_reference(inp)
72+
ref = torch.feature_dropout(ref_inp, 0.0, True)
73+
with flag_gems.use_gems():
74+
res_out = torch.feature_dropout(inp, 0.0, True)
75+
gems_assert_close(res_out, ref, dtype)
76+
77+
78+
@pytest.mark.feature_dropout
79+
@pytest.mark.parametrize("shape", FEATURE_DROPOUT_SHAPES)
80+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
81+
def test_feature_dropout_p_one(shape, dtype):
82+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
83+
ref = to_reference(torch.zeros_like(inp))
84+
with flag_gems.use_gems():
85+
res_out = torch.feature_dropout(inp, 1.0, True)
86+
gems_assert_equal(res_out, ref)
87+
88+
89+
@pytest.mark.feature_dropout_
90+
@pytest.mark.parametrize("shape", FEATURE_DROPOUT_SHAPES)
91+
@pytest.mark.parametrize("p", [0.3, 0.5])
92+
@pytest.mark.parametrize("dtype", FLOAT_DTYPES)
93+
def test_feature_dropout_inplace(shape, p, dtype):
94+
inp = torch.randn(shape, dtype=dtype, device=flag_gems.device)
95+
inp_clone = inp.clone()
96+
with flag_gems.use_gems():
97+
res_out = torch.feature_dropout_(inp, p, True)
98+
assert res_out.data_ptr() == inp.data_ptr()
99+
batch_size, num_channels = shape[0], shape[1]
100+
scale = 1.0 / (1.0 - p)
101+
inp_reshaped = inp_clone.view(batch_size, num_channels, -1)
102+
out_reshaped = res_out.view(batch_size, num_channels, -1)
103+
for b in range(batch_size):
104+
for c in range(num_channels):
105+
channel_out = out_reshaped[b, c]
106+
channel_inp = inp_reshaped[b, c]
107+
if not torch.all(channel_out == 0).item():
108+
assert torch.allclose(
109+
channel_out, channel_inp * scale, rtol=1e-4, atol=1e-5
110+
)

0 commit comments

Comments
 (0)