Skip to content

Commit 91fede8

Browse files
committed
feat: add feature_dropout operator implementation, tests and benchmark
1 parent 93e4d30 commit 91fede8

5 files changed

Lines changed: 384 additions & 0 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import pytest
2+
import torch
3+
4+
from benchmark.attri_util import FLOAT_DTYPES
5+
from benchmark.performance_utils import GenericBenchmarkExcluse1D
6+
7+
8+
def feature_dropout_input_fn(shape, dtype, device):
9+
inp = torch.randn(shape, dtype=dtype, device=device)
10+
yield inp, 0.5, True
11+
12+
13+
class FeatureDropoutBenchmark(GenericBenchmarkExcluse1D):
14+
def set_shapes(self, shape_file_path=None):
15+
self.shapes = [
16+
(8, 64),
17+
(16, 128),
18+
(32, 256),
19+
(4, 64, 32, 32),
20+
(8, 128, 16, 16),
21+
(16, 256, 8, 8),
22+
(2, 512, 14, 14),
23+
(4, 1024, 7, 7),
24+
]
25+
26+
def set_more_shapes(self):
27+
return None
28+
29+
30+
@pytest.mark.feature_dropout
31+
def test_perf_feature_dropout():
32+
bench = FeatureDropoutBenchmark(
33+
input_fn=feature_dropout_input_fn,
34+
op_name="feature_dropout",
35+
torch_op=torch.feature_dropout,
36+
dtypes=FLOAT_DTYPES,
37+
)
38+
bench.run()

src/flag_gems/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ def torch_ge(v):
197197
("expm1_", expm1_),
198198
("expm1.out", expm1_out),
199199
("exponential_", exponential_),
200+
("feature_dropout", feature_dropout),
201+
("feature_dropout_", feature_dropout_),
200202
("eye", eye),
201203
("eye.m", eye_m),
202204
("fill.Scalar", fill_scalar),

src/flag_gems/ops/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@
113113
from flag_gems.ops.exponential_ import exponential_
114114
from flag_gems.ops.eye import eye
115115
from flag_gems.ops.eye_m import eye_m
116+
from flag_gems.ops.feature_dropout import feature_dropout, feature_dropout_
116117
from flag_gems.ops.fill import (
117118
fill_scalar,
118119
fill_scalar_,
@@ -459,6 +460,8 @@
459460
"expm1_",
460461
"expm1_out",
461462
"exponential_",
463+
"feature_dropout",
464+
"feature_dropout_",
462465
"eye",
463466
"eye_m",
464467
"fill_scalar",
Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
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+
191+
if not train or p == 0:
192+
return input
193+
194+
if p == 1:
195+
input.zero_()
196+
return input
197+
198+
if input.ndim < 2:
199+
raise RuntimeError(
200+
"Feature dropout requires at least 2 dimensions in the input"
201+
)
202+
203+
assert 0.0 < p < 1.0, "p must be in (0, 1)"
204+
205+
device = input.device
206+
if not input.is_contiguous():
207+
raise RuntimeError("In-place feature dropout requires contiguous input")
208+
209+
batch_size = input.shape[0]
210+
num_channels = input.shape[1]
211+
spatial_size = 1
212+
for i in range(2, input.ndim):
213+
spatial_size *= input.shape[i]
214+
215+
N = batch_size
216+
C = num_channels
217+
numel = input.numel()
218+
scale = 1.0 / (1.0 - p)
219+
220+
mask = torch.empty(N, C, device=device, dtype=torch.float32)
221+
222+
BLOCK_N = min(triton.next_power_of_2(N), 64)
223+
BLOCK_C = min(triton.next_power_of_2(C), 64)
224+
grid_mask = (triton.cdiv(N, BLOCK_N), triton.cdiv(C, BLOCK_C))
225+
226+
increment = triton.cdiv(N * C, 4) * 4
227+
with torch_device_fn.device(device):
228+
philox_seed, philox_offset = philox_backend_seed_offset(increment)
229+
generate_feature_mask_kernel[grid_mask](
230+
mask, N, C, p, scale, philox_seed, philox_offset, BLOCK_N, BLOCK_C
231+
)
232+
233+
BLOCK = 1024
234+
grid_apply = (triton.cdiv(numel, BLOCK),)
235+
236+
with torch_device_fn.device(device):
237+
apply_feature_mask_kernel[grid_apply](
238+
input, input, mask, numel, N, C, spatial_size, BLOCK
239+
)
240+
241+
return input

0 commit comments

Comments
 (0)