Skip to content

Commit d04645c

Browse files
[KernelGen][Nvidia] Add _amp_foreach_non_finite_check_and_unscale_ operator with Triton kernel (flagos-ai#3799)
* [KernelGen][Nvidia] Add _amp_foreach_non_finite_check_and_unscale_ operator with Triton kernel * fix: use to_reference for inv_scale in reference path When running with --ref cpu, ref_tensors and ref_found_inf are moved to CPU but inv_scale stayed on CUDA, causing a device mismatch error in the reference call to torch._amp_foreach_non_finite_check_and_unscale_. * Update operator stage to alpha 5.4 * fix: address PR check findings for _amp_foreach_non_finite_check_and_unscale_ * fix: sort ops __all__ for amp foreach --------- Co-authored-by: yangy0906 <yangyang0906c@163.com>
1 parent b69c1e9 commit d04645c

6 files changed

Lines changed: 339 additions & 3 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
from typing import Generator
2+
3+
import pytest
4+
import torch
5+
6+
from . import base
7+
8+
9+
class AmpForeachNonFiniteCheckAndUnscaleBenchmark(base.Benchmark):
10+
"""
11+
Benchmark class for _amp_foreach_non_finite_check_and_unscale_ operation.
12+
This operation takes a list of tensors, a found_inf tensor, and an inv_scale
13+
scalar tensor, and checks for non-finite values while unscaling.
14+
"""
15+
16+
# Common sizes for gradient tensors in training scenarios covering small to medium workloads
17+
DEFAULT_SHAPES = [
18+
(1024, 1024),
19+
(2048, 2048),
20+
(4096, 4096),
21+
]
22+
# shapes describe the first tensor; a second tensor at half size is generated
23+
DEFAULT_SHAPE_DESC = "M, N"
24+
25+
def set_more_shapes(self):
26+
# larger shapes for comprehensive benchmark level
27+
more_shapes_2d = [(1024, 2**i) for i in range(2, 14, 4)]
28+
more_shapes_3d = [(64, 2**i, 64) for i in range(2, 10, 4)]
29+
return more_shapes_2d + more_shapes_3d
30+
31+
def get_input_iter(self, dtype) -> Generator:
32+
for shape in self.shapes:
33+
# generate 2 tensors: one at shape, one at half size in the first dim
34+
second_shape = (max(1, shape[0] // 2),) + shape[1:]
35+
tensors = [
36+
torch.randn(shape, device=self.device, dtype=dtype),
37+
torch.randn(second_shape, device=self.device, dtype=dtype),
38+
]
39+
# PyTorch expects inv_scale and found_inf as float32
40+
inv_scale = torch.tensor(2.0, device=self.device, dtype=torch.float32)
41+
found_inf = torch.tensor(0.0, device=self.device, dtype=torch.float32)
42+
yield tensors, found_inf, inv_scale
43+
44+
45+
@pytest.mark.amp_foreach_non_finite_check_and_unscale_
46+
def test_amp_foreach_non_finite_check_and_unscale_():
47+
bench = AmpForeachNonFiniteCheckAndUnscaleBenchmark(
48+
op_name="amp_foreach_non_finite_check_and_unscale_",
49+
torch_op=torch._amp_foreach_non_finite_check_and_unscale_,
50+
# bfloat16 is not supported by the CUDA kernel for this operator
51+
dtypes=[torch.float16, torch.float32],
52+
)
53+
bench.run()

conf/operators.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,20 @@ ops:
436436
- Tensor
437437
stages:
438438
- beta: '5.3'
439+
- id: amp_foreach_non_finite_check_and_unscale_
440+
description: |
441+
Checks gradient tensors for non-finite values (inf or nan) and
442+
unscales each tensor in-place by `inv_scale`. If any non-finite
443+
value is detected, `found_inf` is set to 1.0.
444+
for:
445+
- _amp_foreach_non_finite_check_and_unscale_
446+
labels:
447+
- aten
448+
- KernelGen
449+
kind:
450+
- Math
451+
stages:
452+
- alpha: '5.4'
439453
- id: angle
440454
description: Computes the element-wise angle (in radians) of the given `input` tensor.
441455
for:

src/flag_gems/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ def torch_ge(v):
4343
("__or__.Scalar", bitwise_or_scalar),
4444
("__or__.Tensor", bitwise_or_tensor),
4545
("_adaptive_avg_pool2d", adaptive_avg_pool2d),
46+
(
47+
"_amp_foreach_non_finite_check_and_unscale_",
48+
_amp_foreach_non_finite_check_and_unscale_,
49+
),
4650
("_assert_async", _assert_async),
4751
("_cdist_backward", _cdist_backward),
4852
("_conv_depthwise2d", _conv_depthwise2d),

src/flag_gems/ops/__init__.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from flag_gems.ops.__ilshift__ import __ilshift__
2+
from flag_gems.ops._amp_foreach_non_finite_check_and_unscale_ import (
3+
_amp_foreach_non_finite_check_and_unscale_,
4+
)
25
from flag_gems.ops._euclidean_dist import _euclidean_dist
36
from flag_gems.ops._functional_sym_constrain_range_for_size import (
47
_functional_sym_constrain_range_for_size,
@@ -482,6 +485,7 @@
482485
"SUPPORTED_FP8_DTYPE",
483486
"ScaleDotProductAttention",
484487
"__ilshift__",
488+
"_amp_foreach_non_finite_check_and_unscale_",
485489
"_assert_async",
486490
"_cdist_backward",
487491
"_conv_depthwise2d",
@@ -674,13 +678,13 @@
674678
"feature_dropout",
675679
"feature_dropout_",
676680
"fft",
677-
"fix",
678681
"fill_scalar",
679682
"fill_scalar_",
680683
"fill_scalar_out",
681684
"fill_tensor",
682685
"fill_tensor_",
683686
"fill_tensor_out",
687+
"fix",
684688
"flash_attention_backward",
685689
"flash_attention_forward",
686690
"flash_attn_varlen_func",
@@ -857,12 +861,12 @@
857861
"nonzero",
858862
"nonzero_numpy",
859863
"normal_",
860-
"not_equal",
861-
"not_equal_scalar",
862864
"normal_float_tensor",
863865
"normal_tensor_float",
864866
"normal_tensor_tensor",
865867
"normed_cumsum",
868+
"not_equal",
869+
"not_equal_scalar",
866870
"one_hot",
867871
"ones",
868872
"ones_like",
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
# Copyright 2026, The FlagOS Contributors.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
#
15+
# Generated by KernelGen: https://github.com/flagos-ai/KernelGen
16+
import logging
17+
from typing import List
18+
19+
import torch
20+
import triton
21+
import triton.language as tl
22+
23+
from flag_gems.utils import tl_extra_shim
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
@triton.jit
29+
def _amp_foreach_non_finite_check_and_unscale_kernel(
30+
inp_ptr,
31+
inv_scale_ptr,
32+
output_ptr,
33+
num_elements,
34+
BLOCK_SIZE: tl.constexpr,
35+
):
36+
pid = tl.program_id(0)
37+
block_start = pid * BLOCK_SIZE
38+
offsets = tl.arange(0, BLOCK_SIZE)
39+
mask = block_start + offsets < num_elements
40+
41+
# Load the input value
42+
inp = tl.load(inp_ptr + block_start + offsets, mask=mask, other=0.0)
43+
44+
# Load the inv_scale value
45+
scale = tl.load(inv_scale_ptr)
46+
47+
# Convert to float32 for computation to handle float16/bfloat16
48+
inp_fp32 = inp.to(tl.float32)
49+
scale_fp32 = scale.to(tl.float32)
50+
51+
# Check for non-finite values (inf or nan) using float32
52+
is_non_finite = ~tl_extra_shim.finitef(inp_fp32)
53+
54+
# Scale the values: only finite values are scaled
55+
# Non-finite values stay as-is (in original dtype)
56+
scaled_fp32 = tl.where(is_non_finite, inp_fp32, inp_fp32 * scale_fp32)
57+
58+
# Convert back to original dtype
59+
# We need to determine the output dtype from the input pointer
60+
# For now, assume input dtype is preserved
61+
scaled = scaled_fp32.to(inp.dtype)
62+
63+
# Store the result
64+
tl.store(output_ptr + block_start + offsets, scaled, mask=mask)
65+
66+
67+
def _amp_foreach_non_finite_check_and_unscale_(
68+
tensors: List[torch.Tensor],
69+
found_inf: torch.Tensor,
70+
inv_scale: torch.Tensor,
71+
):
72+
"""
73+
Check for non-finite values in tensors and unscale them.
74+
75+
For each tensor in the list:
76+
- Scale finite values by inv_scale
77+
- Non-finite values (inf, nan) remain unchanged
78+
79+
If any tensor has non-finite values, set found_inf to 1.0.
80+
"""
81+
logger.debug("GEMS AMP_FOREACH_NON_FINITE_CHECK_AND_UNSCALE")
82+
83+
if not isinstance(tensors, (list, tuple)):
84+
raise TypeError(f"Expected list or tuple of tensors, got {type(tensors)}")
85+
86+
if len(tensors) == 0:
87+
return
88+
89+
# Ensure inv_scale is a float32 tensor as PyTorch expects
90+
inv_scale = inv_scale.to(dtype=torch.float32)
91+
92+
# Process each tensor in the list
93+
for tensor in tensors:
94+
if not tensor.is_floating_point():
95+
# Skip non-floating point tensors
96+
continue
97+
98+
num_elements = tensor.numel()
99+
if num_elements == 0:
100+
continue
101+
102+
# Allocate output tensor (in-place operation)
103+
output = torch.empty_like(tensor)
104+
105+
# 1024 offers a good balance of occupancy and parallelism for typical gradient tensor sizes
106+
BLOCK_SIZE = 1024
107+
grid = (triton.cdiv(num_elements, BLOCK_SIZE),)
108+
109+
_amp_foreach_non_finite_check_and_unscale_kernel[grid](
110+
tensor,
111+
inv_scale,
112+
output,
113+
num_elements,
114+
BLOCK_SIZE=BLOCK_SIZE,
115+
)
116+
117+
# Copy output back to input (in-place)
118+
tensor.copy_(output)
119+
120+
# Check if any tensor has non-finite values
121+
# This needs to be done after scaling since scaling might produce inf from overflow
122+
for tensor in tensors:
123+
if not tensor.is_floating_point():
124+
continue
125+
126+
if tensor.is_complex():
127+
# Check real and imaginary parts separately
128+
if not torch.all(torch.isfinite(tensor.real)) or not torch.all(
129+
torch.isfinite(tensor.imag)
130+
):
131+
found_inf.fill_(1.0)
132+
return
133+
else:
134+
if not torch.all(torch.isfinite(tensor)):
135+
found_inf.fill_(1.0)
136+
return
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import pytest
2+
import torch
3+
4+
import flag_gems
5+
6+
from . import accuracy_utils as utils
7+
8+
# worktree explicitly excludes bfloat16 via PRIMARY_FLOAT_DTYPES
9+
PRIMARY_FLOAT_DTYPES = [torch.float16, torch.float32]
10+
11+
12+
@pytest.mark.amp_foreach_non_finite_check_and_unscale_
13+
@pytest.mark.parametrize("dtype", PRIMARY_FLOAT_DTYPES)
14+
def test_amp_foreach_non_finite_check_and_unscale_(dtype):
15+
"""Test _amp_foreach_non_finite_check_and_unscale_ with normal tensors."""
16+
# PyTorch expects inv_scale and found_inf to be float32
17+
inv_scale = torch.tensor(2.0, device=flag_gems.device, dtype=torch.float32)
18+
found_inf = torch.tensor(0.0, device=flag_gems.device, dtype=torch.float32)
19+
20+
tensors = [
21+
torch.randn(16, 32, device=flag_gems.device, dtype=dtype),
22+
torch.randn(8, 16, device=flag_gems.device, dtype=dtype),
23+
]
24+
25+
# Reference
26+
ref_tensors = [utils.to_reference(t.clone()) for t in tensors]
27+
ref_found_inf = utils.to_reference(found_inf.clone())
28+
ref_inv_scale = utils.to_reference(inv_scale.clone())
29+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
30+
ref_tensors, ref_found_inf, ref_inv_scale
31+
)
32+
33+
# GEMS
34+
res_tensors = [t.clone() for t in tensors]
35+
res_found_inf = found_inf.clone()
36+
with flag_gems.use_gems():
37+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
38+
res_tensors, res_found_inf, inv_scale
39+
)
40+
41+
# Compare mutated inputs (in-place operation)
42+
for i, (inp, ref_inp) in enumerate(zip(res_tensors, ref_tensors)):
43+
utils.gems_assert_close(inp, ref_inp, dtype)
44+
45+
# Compare found_inf (also mutated in-place)
46+
utils.gems_assert_equal(res_found_inf, ref_found_inf)
47+
48+
49+
@pytest.mark.amp_foreach_non_finite_check_and_unscale_
50+
@pytest.mark.parametrize("dtype", PRIMARY_FLOAT_DTYPES)
51+
def test_amp_foreach_non_finite_check_and_unscale__inf(dtype):
52+
"""Test _amp_foreach_non_finite_check_and_unscale_ with inf values."""
53+
# PyTorch expects inv_scale and found_inf to be float32
54+
inv_scale = torch.tensor(2.0, device=flag_gems.device, dtype=torch.float32)
55+
found_inf = torch.tensor(0.0, device=flag_gems.device, dtype=torch.float32)
56+
57+
tensors = [
58+
torch.tensor(
59+
[1.0, 2.0, float("inf"), 4.0], device=flag_gems.device, dtype=dtype
60+
),
61+
torch.tensor([5.0, 6.0, 7.0], device=flag_gems.device, dtype=dtype),
62+
]
63+
64+
# Reference
65+
ref_tensors = [utils.to_reference(t.clone()) for t in tensors]
66+
ref_found_inf = utils.to_reference(found_inf.clone())
67+
ref_inv_scale = utils.to_reference(inv_scale.clone())
68+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
69+
ref_tensors, ref_found_inf, ref_inv_scale
70+
)
71+
72+
# GEMS
73+
res_tensors = [t.clone() for t in tensors]
74+
res_found_inf = found_inf.clone()
75+
with flag_gems.use_gems():
76+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
77+
res_tensors, res_found_inf, inv_scale
78+
)
79+
80+
# Compare mutated inputs (in-place operation)
81+
# Note: inf values remain unchanged, only finite values are scaled
82+
for i, (inp, ref_inp) in enumerate(zip(res_tensors, ref_tensors)):
83+
utils.gems_assert_close(inp, ref_inp, dtype)
84+
85+
# Compare found_inf (also mutated in-place) - should be 1.0 when inf is present
86+
utils.gems_assert_equal(res_found_inf, ref_found_inf)
87+
88+
89+
@pytest.mark.amp_foreach_non_finite_check_and_unscale_
90+
@pytest.mark.parametrize("dtype", PRIMARY_FLOAT_DTYPES)
91+
def test_amp_foreach_non_finite_check_and_unscale__nan(dtype):
92+
"""Test _amp_foreach_non_finite_check_and_unscale_ with nan values."""
93+
# PyTorch expects inv_scale and found_inf to be float32
94+
inv_scale = torch.tensor(2.0, device=flag_gems.device, dtype=torch.float32)
95+
found_inf = torch.tensor(0.0, device=flag_gems.device, dtype=torch.float32)
96+
97+
tensors = [
98+
torch.tensor(
99+
[1.0, 2.0, float("nan"), 4.0], device=flag_gems.device, dtype=dtype
100+
),
101+
torch.tensor([5.0, 6.0, 7.0], device=flag_gems.device, dtype=dtype),
102+
]
103+
104+
# Reference
105+
ref_tensors = [utils.to_reference(t.clone()) for t in tensors]
106+
ref_found_inf = utils.to_reference(found_inf.clone())
107+
ref_inv_scale = utils.to_reference(inv_scale.clone())
108+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
109+
ref_tensors, ref_found_inf, ref_inv_scale
110+
)
111+
112+
# GEMS
113+
res_tensors = [t.clone() for t in tensors]
114+
res_found_inf = found_inf.clone()
115+
with flag_gems.use_gems():
116+
getattr(torch, "_amp_foreach_non_finite_check_and_unscale_")(
117+
res_tensors, res_found_inf, inv_scale
118+
)
119+
120+
# Compare mutated inputs with equal_nan=True since tensors contain NaN
121+
for i, (inp, ref_inp) in enumerate(zip(res_tensors, ref_tensors)):
122+
utils.gems_assert_close(inp, ref_inp, dtype, equal_nan=True)
123+
124+
# Compare found_inf - should be 1.0 when nan is present
125+
utils.gems_assert_equal(res_found_inf, ref_found_inf)

0 commit comments

Comments
 (0)