Skip to content

Commit ae91169

Browse files
authored
fix: resolve merge conflict for _jagged_to_padded_dense_forward PR (flagos-ai#3809)
Rebase onto origin/master (6f0fa85): resolved ops/__init__.py import/__all__ conflict by rebuilding PR changes cleanly from origin/master. Added operator entry in correct alphabetical position across all registration files.
1 parent d04645c commit ae91169

6 files changed

Lines changed: 267 additions & 0 deletions

File tree

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import numpy as np
2+
import pytest
3+
import torch
4+
5+
from . import base, consts
6+
7+
# Jagged to padded dense forward benchmark
8+
JAGGED_TO_PADDED_SHAPES = [
9+
(8, 8),
10+
(16, 16),
11+
(32, 32),
12+
(64, 64),
13+
(128, 64),
14+
(256, 128),
15+
(512, 256),
16+
]
17+
18+
19+
class JaggedToPaddedDenseForwardBenchmark(base.Benchmark):
20+
def set_shapes(self, shape_file_path=None):
21+
self.shapes = JAGGED_TO_PADDED_SHAPES
22+
23+
def get_input_iter(self, cur_dtype):
24+
for batch_size, max_length in self.shapes:
25+
# Generate random sequence lengths
26+
np.random.seed(42)
27+
seq_lengths = np.random.randint(1, max_length + 1, size=batch_size).tolist()
28+
29+
# Create offsets tensor (cumulative)
30+
offsets = [0] + list(np.cumsum(seq_lengths).astype(int).tolist())
31+
offsets = torch.tensor(offsets, device=self.device, dtype=torch.int64)
32+
33+
# Create values tensor (concatenated sequences)
34+
total_length = sum(seq_lengths)
35+
values = torch.randn(total_length, dtype=cur_dtype, device=self.device)
36+
37+
yield values, [offsets], [max_length], 0.0
38+
39+
40+
@pytest.mark.jagged_to_padded_dense_forward
41+
def test_jagged_to_padded_dense_forward():
42+
bench = JaggedToPaddedDenseForwardBenchmark(
43+
op_name="jagged_to_padded_dense_forward",
44+
torch_op=torch.ops.aten._jagged_to_padded_dense_forward,
45+
dtypes=consts.FLOAT_DTYPES,
46+
)
47+
bench.run()

conf/operators.yaml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3754,6 +3754,18 @@ ops:
37543754
- Math
37553755
stages:
37563756
- stable: '5.3'
3757+
- id: jagged_to_padded_dense_forward
3758+
description: |
3759+
Converts a jagged (variable-length) tensor to a padded dense tensor.
3760+
for:
3761+
- _jagged_to_padded_dense_forward
3762+
labels:
3763+
- aten
3764+
- KernelGen
3765+
kind:
3766+
- Tensor
3767+
stages:
3768+
- alpha: '5.4'
37573769
- id: kron
37583770
description: Computes the Kronecker product of `input` and `other`.
37593771
for:

src/flag_gems/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def torch_ge(v):
6161
("_grouped_mm", group_mm),
6262
("_index_put_impl_", _index_put_impl_),
6363
("_is_all_true", _is_all_true),
64+
("_jagged_to_padded_dense_forward", _jagged_to_padded_dense_forward),
6465
("_linalg_eigvals", _linalg_eigvals),
6566
("_log_softmax", log_softmax),
6667
("_log_softmax.out", log_softmax_out),

src/flag_gems/ops/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
_functional_sym_constrain_range_for_size,
88
)
99
from flag_gems.ops._is_all_true import _is_all_true
10+
from flag_gems.ops._jagged_to_padded_dense_forward import (
11+
_jagged_to_padded_dense_forward,
12+
)
1013
from flag_gems.ops._linalg_eigvals import _linalg_eigvals
1114
from flag_gems.ops._resize_output import _resize_output
1215
from flag_gems.ops._safe_softmax import _safe_softmax
@@ -493,6 +496,7 @@
493496
"_functional_sym_constrain_range_for_size",
494497
"_index_put_impl_",
495498
"_is_all_true",
499+
"_jagged_to_padded_dense_forward",
496500
"_linalg_eigvals",
497501
"_resize_output",
498502
"_safe_softmax",
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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+
18+
import torch
19+
import triton
20+
import triton.language as tl
21+
22+
from flag_gems.utils import libentry
23+
from flag_gems.utils import triton_lang_extension as tle
24+
25+
logger = logging.getLogger(__name__)
26+
27+
28+
@libentry()
29+
@triton.jit
30+
def _jagged_to_padded_dense_forward_kernel(
31+
values,
32+
offsets,
33+
output,
34+
padding_value: tl.constexpr,
35+
batch_size: tl.constexpr,
36+
max_length: tl.constexpr,
37+
BLOCK_SIZE: tl.constexpr,
38+
):
39+
"""Kernel for converting jagged tensor to padded dense tensor.
40+
41+
Args:
42+
values: 1D tensor containing concatenated variable-length sequences
43+
offsets: 1D tensor of start positions for each sequence
44+
output: 2D output tensor of shape (batch_size, max_length)
45+
padding_value: scalar value for padding
46+
batch_size: number of sequences
47+
max_length: maximum length of each sequence
48+
"""
49+
pid = tle.program_id(axis=0)
50+
batch_idx = pid
51+
52+
if batch_idx >= batch_size:
53+
return
54+
55+
# Get the start and end offset for this sequence
56+
seq_start = tl.load(offsets + batch_idx)
57+
seq_end = tl.load(offsets + batch_idx + 1)
58+
59+
# Calculate the actual sequence length
60+
seq_length = seq_end - seq_start
61+
62+
# Compute the row offset in the output
63+
row_offset = batch_idx * max_length
64+
65+
# Fill with padding value (vectorized per block)
66+
for j in tl.range(0, max_length, BLOCK_SIZE):
67+
out_offsets = row_offset + j + tl.arange(0, BLOCK_SIZE)
68+
out_mask = (j + tl.arange(0, BLOCK_SIZE)) < max_length
69+
tl.store(output + out_offsets, padding_value, mask=out_mask)
70+
71+
# Copy actual values (vectorized per block)
72+
for j in tl.range(0, seq_length, BLOCK_SIZE):
73+
offsets_vec = seq_start + j + tl.arange(0, BLOCK_SIZE)
74+
mask = offsets_vec < seq_end
75+
76+
values_vec = tl.load(values + offsets_vec, mask=mask, other=padding_value)
77+
78+
out_offsets = row_offset + j + tl.arange(0, BLOCK_SIZE)
79+
out_mask = (j + tl.arange(0, BLOCK_SIZE)) < seq_length
80+
81+
tl.store(output + out_offsets, values_vec, mask=out_mask)
82+
83+
84+
def _jagged_to_padded_dense_forward(values, offsets, max_lengths, padding_value=0.0):
85+
"""Convert a jagged (variable-length) tensor to a padded dense tensor.
86+
87+
Args:
88+
values: 1D tensor containing concatenated variable-length sequences
89+
offsets: List of 1D tensors containing start positions for each sequence
90+
max_lengths: List of integers specifying maximum length for each batch dimension
91+
padding_value: Value to use for padding (default: 0.0)
92+
93+
Returns:
94+
Padded dense tensor
95+
"""
96+
logger.debug("GEMS JAGGED TO PADDED DENSE FORWARD")
97+
98+
# Currently only supports single batch dimension
99+
if not isinstance(offsets, (list, tuple)):
100+
offsets = [offsets]
101+
if not isinstance(max_lengths, (list, tuple)):
102+
max_lengths = [max_lengths]
103+
104+
num_batch_dims = len(offsets)
105+
assert (
106+
num_batch_dims == 1
107+
), f"Only single batch dimension is supported, got {num_batch_dims}"
108+
109+
# Single batch dimension: 1D values, 1D offsets
110+
offsets_0 = offsets[0]
111+
batch_size = offsets_0.numel() - 1
112+
max_length = max_lengths[0]
113+
114+
# Compute output shape
115+
# For single batch dim: (batch_size, max_length)
116+
output_shape = (batch_size, max_length)
117+
output = torch.empty(output_shape, dtype=values.dtype, device=values.device)
118+
119+
grid = lambda meta: (batch_size,)
120+
_jagged_to_padded_dense_forward_kernel[grid](
121+
values,
122+
offsets_0,
123+
output,
124+
padding_value,
125+
batch_size,
126+
max_length,
127+
# BLOCK_SIZE=128 balances occupancy and memory efficiency for typical sequence lengths
128+
BLOCK_SIZE=128,
129+
)
130+
131+
return output
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import numpy as np
2+
import pytest
3+
import torch
4+
5+
import flag_gems
6+
7+
from . import accuracy_utils as utils
8+
9+
10+
@pytest.mark.jagged_to_padded_dense_forward
11+
@pytest.mark.parametrize("batch_size", [1, 8, 32, 128])
12+
@pytest.mark.parametrize("max_length", [8, 16, 32, 128])
13+
@pytest.mark.parametrize("dtype", utils.FLOAT_DTYPES)
14+
def test_jagged_to_padded_dense_forward(batch_size, max_length, dtype):
15+
# Create variable-length sequences
16+
# Generate random sequence lengths
17+
np.random.seed(42)
18+
seq_lengths = np.random.randint(1, max_length + 1, size=batch_size).tolist()
19+
20+
# Create offsets tensor (cumulative)
21+
offsets = [0] + list(np.cumsum(seq_lengths).astype(int).tolist())
22+
offsets = torch.tensor(offsets, device=flag_gems.device, dtype=torch.int64)
23+
24+
# Create values tensor (concatenated sequences)
25+
total_length = sum(seq_lengths)
26+
values = torch.randn(total_length, dtype=dtype, device=flag_gems.device)
27+
28+
# Reference implementation
29+
ref_values = utils.to_reference(values)
30+
ref_offsets = utils.to_reference(offsets)
31+
32+
ref_out = torch.ops.aten._jagged_to_padded_dense_forward(
33+
ref_values, [ref_offsets], [max_length], 0.0
34+
)
35+
36+
# GEMS implementation
37+
with flag_gems.use_gems():
38+
res_out = torch.ops.aten._jagged_to_padded_dense_forward(
39+
values, [offsets], [max_length], 0.0
40+
)
41+
42+
utils.gems_assert_close(res_out, ref_out, dtype)
43+
44+
45+
@pytest.mark.jagged_to_padded_dense_forward
46+
@pytest.mark.parametrize("batch_size", [8, 32])
47+
@pytest.mark.parametrize("max_length", [16, 32])
48+
@pytest.mark.parametrize("padding_value", [0.0, -1.0, 1.5])
49+
def test_jagged_to_padded_dense_forward_padding(batch_size, max_length, padding_value):
50+
# Test with different padding values
51+
np.random.seed(42)
52+
seq_lengths = np.random.randint(1, max_length + 1, size=batch_size).tolist()
53+
54+
offsets = [0] + list(np.cumsum(seq_lengths).astype(int).tolist())
55+
offsets = torch.tensor(offsets, device=flag_gems.device, dtype=torch.int64)
56+
57+
total_length = sum(seq_lengths)
58+
values = torch.randn(total_length, dtype=torch.float32, device=flag_gems.device)
59+
60+
ref_values = utils.to_reference(values)
61+
ref_offsets = utils.to_reference(offsets)
62+
63+
ref_out = torch.ops.aten._jagged_to_padded_dense_forward(
64+
ref_values, [ref_offsets], [max_length], padding_value
65+
)
66+
67+
with flag_gems.use_gems():
68+
res_out = torch.ops.aten._jagged_to_padded_dense_forward(
69+
values, [offsets], [max_length], padding_value
70+
)
71+
72+
utils.gems_assert_close(res_out, ref_out, torch.float32)

0 commit comments

Comments
 (0)