Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/flydsl/Dialect/Fly/IR/FlyOps.td
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ def Fly_SimpleLayoutCoordTensor : Constraint<
def Fly_ComposedLayoutMemRef : Constraint<Neg<Fly_SimpleLayoutMemRef.predicate>>;
def Fly_ComposedLayoutCoordTensor : Constraint<Neg<Fly_SimpleLayoutCoordTensor.predicate>>;

// The offsets of `add_offset(add_offset($0, $1), $2)` may be merged: either $0 is not a
// shared (LDS) pointer, or $1 and $2 are both compile-time or both runtime.
def Fly_MergeableOffsetChain : Constraint<CPred<[{
!fly::isGenericAddressSpace<fly::AddressSpace::Shared>(
cast<fly::PointerType>($0.getType()).getAddressSpace()) ||
cast<fly::IntTupleType>($1.getType()).getAttr().isStatic() ==
cast<fly::IntTupleType>($2.getType()).getAttr().isStatic()
}]>>;

//===----------------------------------------------------------------------===//
// Constructors
//===----------------------------------------------------------------------===//
Expand Down
6 changes: 5 additions & 1 deletion include/flydsl/Dialect/Fly/Transforms/MemrefLowering.td
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,12 @@ def : Pat<(Fly_PtrLoadOp Fly_IntTuple:$int_tuple),

def : Pat<(Fly_AddOffsetOp Fly_IntTuple:$int_tuple, Fly_IntTuple:$offset),
(Fly_IntTupleAddOp $int_tuple, $offset)>;
// Merging a compile-time offset into a runtime one hides the constant inside a dynamic
// getelementptr index, which costs an LDS access its `ds_read ... offset:` immediate and a
// shared base register. Leave mixed chains on a shared pointer nested; see #898.
def : Pat<(Fly_AddOffsetOp (Fly_AddOffsetOp Fly_Pointer:$ptr, Fly_IntTuple:$offset1), Fly_IntTuple:$offset2),
(Fly_AddOffsetOp $ptr, (Fly_IntTupleAddOp $offset1, $offset2))>;
(Fly_AddOffsetOp $ptr, (Fly_IntTupleAddOp $offset1, $offset2)),
[(Fly_MergeableOffsetChain $ptr, $offset1, $offset2)]>;

def : Pat<(Fly_DecompositionOp Fly_MemRef:$memref),
(replaceWithValue $memref),
Expand Down
137 changes: 137 additions & 0 deletions tests/kernels/test_lds_offset_chain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
#!/usr/bin/env python3

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 FlyDSL Project Contributors

"""Nested ``add_offset`` chains on shared (LDS) pointers must read the same elements
however the offsets are spelled: constant inner, runtime inner, or pre-merged.
"""

import pytest

import flydsl.compiler as flyc
import flydsl.expr as fx

try:
import torch
except ImportError:
torch = None

pytestmark = [pytest.mark.l2_device, pytest.mark.rocm_lower]

if torch is None or not torch.cuda.is_available():
pytest.skip("CUDA/ROCm not available. Skipping GPU tests.", allow_module_level=True)

BLOCK_DIM = 256
NUM_STAGES = 4
NUM_BLOCKS = 8

# Per-thread LDS read index. Coprime with BLOCK_DIM, so a base/offset split that
# loses the runtime or the compile-time part reads a different element and the
# reference comparison fails instead of aliasing back onto the identity.
LANE_STRIDE = 7


def _stage_sum_body(A, C, block_dim, num_stages, read_ptr):
"""Stage every thread's slice of A into LDS, then reduce across stages.

``read_ptr(s_ptr, stage_elems, lane)`` builds the LDS element address for
stage ``s``; the spelling of that address is what each kernel varies.
"""
tid = fx.thread_idx.x
bid = fx.block_idx.x
tile = block_dim * num_stages

a_ptr = fx.get_iter(A)
c_ptr = fx.get_iter(C)
s_ptr = fx.SharedAllocator().allocate(fx.Array[fx.Float32, tile]).peek().ptr

for s in fx.range_constexpr(num_stages):
s_ptr[s * block_dim + tid] = a_ptr[bid * tile + s * block_dim + tid]
fx.barrier()

lane = (tid * LANE_STRIDE) % block_dim
acc = fx.Float32(0.0)
for s in fx.range_constexpr(num_stages):
acc = acc + read_ptr(s_ptr, s * block_dim, lane).load()
c_ptr[bid * block_dim + tid] = acc


@flyc.kernel
def lds_chain_const_inner_kernel(
A: fx.Tensor,
C: fx.Tensor,
block_dim: fx.Constexpr[int],
num_stages: fx.Constexpr[int],
):
def read_ptr(s_ptr, stage_elems, lane):
stage = fx.add_offset(s_ptr, fx.make_int_tuple(stage_elems)) # compile-time, inner
return fx.add_offset(stage, fx.make_int_tuple(lane)) # runtime, outer

_stage_sum_body(A, C, block_dim, num_stages, read_ptr)


@flyc.kernel
def lds_chain_runtime_inner_kernel(
A: fx.Tensor,
C: fx.Tensor,
block_dim: fx.Constexpr[int],
num_stages: fx.Constexpr[int],
):
def read_ptr(s_ptr, stage_elems, lane):
base = fx.add_offset(s_ptr, fx.make_int_tuple(lane)) # runtime, inner
return fx.add_offset(base, fx.make_int_tuple(stage_elems)) # compile-time, outer

_stage_sum_body(A, C, block_dim, num_stages, read_ptr)


@flyc.kernel
def lds_flat_offset_kernel(
A: fx.Tensor,
C: fx.Tensor,
block_dim: fx.Constexpr[int],
num_stages: fx.Constexpr[int],
):
def read_ptr(s_ptr, stage_elems, lane):
return fx.add_offset(s_ptr, fx.make_int_tuple(lane + stage_elems)) # already merged

_stage_sum_body(A, C, block_dim, num_stages, read_ptr)


def _launcher(kernel):
@flyc.jit
def launch(
A: fx.Tensor,
C: fx.Tensor,
num_blocks: fx.Constexpr[int],
block_dim: fx.Constexpr[int],
num_stages: fx.Constexpr[int],
stream: fx.Stream = fx.Stream(None),
):
kernel(A, C, block_dim, num_stages).launch(grid=(num_blocks, 1, 1), block=(block_dim, 1, 1), stream=stream)

return launch


def _reference(a):
"""out[b, t] = sum_s A[b, s, (t * LANE_STRIDE) % BLOCK_DIM]"""
staged = a.reshape(NUM_BLOCKS, NUM_STAGES, BLOCK_DIM).sum(dim=1)
lane = (torch.arange(BLOCK_DIM, device=a.device) * LANE_STRIDE) % BLOCK_DIM
return staged[:, lane].reshape(-1)


@pytest.mark.parametrize(
"kernel",
[lds_chain_const_inner_kernel, lds_chain_runtime_inner_kernel, lds_flat_offset_kernel],
ids=["const_inner", "runtime_inner", "flat"],
)
def test_lds_nested_add_offset_addressing(kernel):
"""Every spelling of the LDS address chain must read the same elements."""
torch.manual_seed(0)
a = torch.randn(NUM_BLOCKS * NUM_STAGES * BLOCK_DIM, dtype=torch.float32, device="cuda")
c = torch.zeros(NUM_BLOCKS * BLOCK_DIM, dtype=torch.float32, device="cuda")

_launcher(kernel)(a, c, NUM_BLOCKS, BLOCK_DIM, NUM_STAGES, stream=torch.cuda.current_stream())
torch.cuda.synchronize()

torch.testing.assert_close(c, _reference(a), atol=1e-5, rtol=1e-5)
109 changes: 109 additions & 0 deletions tests/mlir/Conversion/add_offset_reassociation.mlir
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright (c) 2026 FlyDSL Project Contributors
// RUN: %fly-opt %s --fly-layout-lowering --convert-fly-to-rocdl | FileCheck %s

// Lowered address arithmetic of a nested add_offset chain, i.e. the shape the AMDGPU
// backend gets to see.
//
// An LDS access keeps its `ds_read ... offset:` immediate only while the compile-time part
// of the address is still a constant getelementptr index: `gep(gep(@lds, %runtime), C)`
// folds C into the instruction and lets every access share one base register, whereas
// `gep(@lds, add(%runtime, C))` hides C inside a dynamic value and makes each access
// materialize its own base. Layout lowering therefore leaves a mixed offset chain on a
// shared pointer nested, and keeps merging offsets everywhere else.
//
// These check the address arithmetic only. tests/kernels/test_lds_offset_chain.py runs both
// mixed orderings on device and checks the values they read against a reference.

// === Shared: the constant stays outside the runtime index ===

// CHECK-LABEL: @test_shared_const_outside_runtime_index
// CHECK-SAME: (%[[PTR:.*]]: !llvm.ptr<3>, %[[OFF:.*]]: i32)
func.func @test_shared_const_outside_runtime_index(%ptr: !fly.ptr<bf16, shared>, %off: i32) -> bf16 {
%o1 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<1024>
// The runtime offset indexes the base once, the constant is a separate gep index.
// CHECK-NOT: arith.addi
// CHECK: %[[BASE:.*]] = llvm.getelementptr %[[PTR]][%[[OFF]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: %[[C1024:.*]] = arith.constant 1024 : i32
// CHECK: %[[ADDR:.*]] = llvm.getelementptr %[[BASE]][%[[C1024]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: llvm.load %[[ADDR]]
// CHECK-NOT: arith.addi
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<1024>) -> !fly.ptr<bf16, shared>
%v = fly.ptr.load(%p2) : (!fly.ptr<bf16, shared>) -> bf16
return %v : bf16
}

// The mirrored chain keeps the constant as its own index too, in the order the author wrote
// it. LLVM folds that inner gep into a constant expression on the LDS symbol, so the reads
// still share one base register and carry their constants in the instruction offset field.
// CHECK-LABEL: @test_shared_const_first
// CHECK-SAME: (%[[PTR:.*]]: !llvm.ptr<3>, %[[OFF:.*]]: i32)
func.func @test_shared_const_first(%ptr: !fly.ptr<bf16, shared>, %off: i32) -> bf16 {
%o1 = fly.make_int_tuple() : () -> !fly.int_tuple<1024>
%o2 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
// CHECK-NOT: arith.addi
// CHECK: %[[C1024:.*]] = arith.constant 1024 : i32
// CHECK: %[[BASE:.*]] = llvm.getelementptr %[[PTR]][%[[C1024]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: %[[ADDR:.*]] = llvm.getelementptr %[[BASE]][%[[OFF]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: llvm.load %[[ADDR]]
// CHECK-NOT: arith.addi
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<1024>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%v = fly.ptr.load(%p2) : (!fly.ptr<bf16, shared>) -> bf16
return %v : bf16
}

// Two compile-time offsets are still folded into one constant index: nothing runtime is
// involved, so there is no constant to lose.
// CHECK-LABEL: @test_shared_all_const
// CHECK-SAME: (%[[PTR:.*]]: !llvm.ptr<3>)
func.func @test_shared_all_const(%ptr: !fly.ptr<bf16, shared>) -> bf16 {
%o1 = fly.make_int_tuple() : () -> !fly.int_tuple<16>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<32>
// CHECK: %[[C48:.*]] = arith.constant 48 : i32
// CHECK: %[[ADDR:.*]] = llvm.getelementptr %[[PTR]][%[[C48]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: llvm.load %[[ADDR]]
// CHECK-NOT: llvm.getelementptr
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<16>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<32>) -> !fly.ptr<bf16, shared>
%v = fly.ptr.load(%p2) : (!fly.ptr<bf16, shared>) -> bf16
return %v : bf16
}

// Two runtime offsets are still merged: one add, one index, nothing constant is buried.
// CHECK-LABEL: @test_shared_all_runtime
// CHECK-SAME: (%[[PTR:.*]]: !llvm.ptr<3>, %[[A:.*]]: i32, %[[B:.*]]: i32)
func.func @test_shared_all_runtime(%ptr: !fly.ptr<bf16, shared>, %a: i32, %b: i32) -> bf16 {
%o1 = fly.make_int_tuple(%a) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple(%b) : (i32) -> !fly.int_tuple<?>
// CHECK: %[[SUM:.*]] = arith.addi %[[A]], %[[B]]
// CHECK: %[[ADDR:.*]] = llvm.getelementptr %[[PTR]][%[[SUM]]] : (!llvm.ptr<3>, i32) -> !llvm.ptr<3>, bf16
// CHECK: llvm.load %[[ADDR]]
// CHECK-NOT: llvm.getelementptr
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%v = fly.ptr.load(%p2) : (!fly.ptr<bf16, shared>) -> bf16
return %v : bf16
}

// === Global: address formation is unchanged ===

// A global pointer keeps the merged form: the two offsets become one index expression and
// one getelementptr, exactly as before the shared-memory rule was added.
// CHECK-LABEL: @test_global_offsets_stay_merged
// CHECK-SAME: (%[[PTR:.*]]: !llvm.ptr<1>, %[[OFF:.*]]: i32)
func.func @test_global_offsets_stay_merged(%ptr: !fly.ptr<f32, global>, %off: i32) -> f32 {
%o1 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK: %[[C8:.*]] = arith.constant 8 : i32
// CHECK: %[[SUM:.*]] = arith.addi %[[OFF]], %[[C8]]
// CHECK: %[[ADDR:.*]] = llvm.getelementptr %[[PTR]][%[[SUM]]] : (!llvm.ptr<1>, i32) -> !llvm.ptr<1>, f32
// CHECK: llvm.load %[[ADDR]]
// CHECK-NOT: llvm.getelementptr
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<f32, global>, !fly.int_tuple<?>) -> !fly.ptr<f32, global>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<f32, global>, !fly.int_tuple<8>) -> !fly.ptr<f32, global>
%v = fly.ptr.load(%p2) : (!fly.ptr<f32, global>) -> f32
return %v : f32
}
93 changes: 93 additions & 0 deletions tests/mlir/Transforms/layout_lowering.mlir
Original file line number Diff line number Diff line change
Expand Up @@ -446,3 +446,96 @@ func.func @test_load_vec_coord_tensor() -> vector<6xi32> {
%vec = fly.memref.load_vec(%ct) : (!fly.coord_tensor<0, (2, 3) : (0, 1)>) -> vector<6xi32>
return %vec : vector<6xi32>
}

// -----

// === Nested add_offset merging ===

// A pointer add_offset becomes a getelementptr index, and an LDS access only keeps its
// `ds_read ... offset:` immediate while the backend still sees that part of the index as a
// constant. Nested offsets on a shared pointer are therefore merged only when both are of
// the same kind; a mixed chain is left nested, in either order. Every other address space
// merges unconditionally, as before.
// See tests/mlir/Conversion/add_offset_reassociation.mlir for the lowered form, and
// tests/kernels/test_lds_offset_chain.py for both mixed orders running on device.

// Two compile-time offsets fold into a single constant offset.
// CHECK-LABEL: @test_add_offset_shared_merge_static
// CHECK-SAME: (%[[PTR:.*]]: !fly.ptr<bf16, shared>)
func.func @test_add_offset_shared_merge_static(%ptr: !fly.ptr<bf16, shared>) -> !fly.ptr<bf16, shared> {
%o1 = fly.make_int_tuple() : () -> !fly.int_tuple<4>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK: %[[C12:.*]] = fly.make_int_tuple() : () -> !fly.int_tuple<12>
// CHECK: fly.add_offset(%[[PTR]], %[[C12]])
// CHECK-NOT: fly.add_offset
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<4>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<8>) -> !fly.ptr<bf16, shared>
return %p2 : !fly.ptr<bf16, shared>
}

// Two runtime offsets merge into a single runtime offset: no constant is lost.
// CHECK-LABEL: @test_add_offset_shared_merge_dynamic
// CHECK-SAME: (%[[PTR:.*]]: !fly.ptr<bf16, shared>, %[[A:.*]]: i32, %[[B:.*]]: i32)
func.func @test_add_offset_shared_merge_dynamic(%ptr: !fly.ptr<bf16, shared>, %a: i32, %b: i32) -> !fly.ptr<bf16, shared> {
%o1 = fly.make_int_tuple(%a) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple(%b) : (i32) -> !fly.int_tuple<?>
// CHECK: %[[SUM:.*]] = arith.addi %[[A]], %[[B]]
// CHECK: %[[OFF:.*]] = fly.make_int_tuple(%[[SUM]])
// CHECK: fly.add_offset(%[[PTR]], %[[OFF]])
// CHECK-NOT: fly.add_offset
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
return %p2 : !fly.ptr<bf16, shared>
}

// A compile-time offset on top of a runtime one is left alone: merging it would hide the
// constant inside a dynamic LDS index and cost every access its own base address.
// CHECK-LABEL: @test_add_offset_shared_mixed_stays_nested
// CHECK-SAME: (%[[PTR:.*]]: !fly.ptr<bf16, shared>, %[[OFF:.*]]: i32)
func.func @test_add_offset_shared_mixed_stays_nested(%ptr: !fly.ptr<bf16, shared>, %off: i32) -> !fly.ptr<bf16, shared> {
%o1 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK-NOT: arith.addi
// CHECK: %[[DYN:.*]] = fly.make_int_tuple(%[[OFF]]) : (i32) -> !fly.int_tuple<?>
// CHECK: %[[C8:.*]] = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK: %[[BASE:.*]] = fly.add_offset(%[[PTR]], %[[DYN]])
// CHECK: fly.add_offset(%[[BASE]], %[[C8]])
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<8>) -> !fly.ptr<bf16, shared>
return %p2 : !fly.ptr<bf16, shared>
}

// The mirrored chain is likewise left alone, in the order it was written: the constant stays
// a getelementptr index of its own, which LLVM folds into a constant expression on the LDS
// symbol rather than into a dynamic index.
// CHECK-LABEL: @test_add_offset_shared_mixed_stays_nested_const_first
// CHECK-SAME: (%[[PTR:.*]]: !fly.ptr<bf16, shared>, %[[OFF:.*]]: i32)
func.func @test_add_offset_shared_mixed_stays_nested_const_first(%ptr: !fly.ptr<bf16, shared>, %off: i32) -> !fly.ptr<bf16, shared> {
%o1 = fly.make_int_tuple() : () -> !fly.int_tuple<8>
%o2 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
// CHECK-NOT: arith.addi
// CHECK: %[[C8:.*]] = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK: %[[DYN:.*]] = fly.make_int_tuple(%[[OFF]]) : (i32) -> !fly.int_tuple<?>
// CHECK: %[[BASE:.*]] = fly.add_offset(%[[PTR]], %[[C8]])
// CHECK: fly.add_offset(%[[BASE]], %[[DYN]])
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<bf16, shared>, !fly.int_tuple<8>) -> !fly.ptr<bf16, shared>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<bf16, shared>, !fly.int_tuple<?>) -> !fly.ptr<bf16, shared>
return %p2 : !fly.ptr<bf16, shared>
}

// A pointer outside shared memory keeps the plain merged form for a mixed chain: it never
// reaches an LDS instruction offset, so one index expression is the canonical shape.
// CHECK-LABEL: @test_add_offset_global_merges_mixed
// CHECK-SAME: (%[[PTR:.*]]: !fly.ptr<f32, global>, %[[OFF:.*]]: i32)
func.func @test_add_offset_global_merges_mixed(%ptr: !fly.ptr<f32, global>, %off: i32) -> !fly.ptr<f32, global> {
%o1 = fly.make_int_tuple(%off) : (i32) -> !fly.int_tuple<?>
%o2 = fly.make_int_tuple() : () -> !fly.int_tuple<8>
// CHECK: %[[C8:.*]] = arith.constant 8 : i32
// CHECK: %[[SUM:.*]] = arith.addi %[[OFF]], %[[C8]]
// CHECK: %[[MERGED:.*]] = fly.make_int_tuple(%[[SUM]])
// CHECK: fly.add_offset(%[[PTR]], %[[MERGED]])
// CHECK-NOT: fly.add_offset
%p1 = fly.add_offset(%ptr, %o1) : (!fly.ptr<f32, global>, !fly.int_tuple<?>) -> !fly.ptr<f32, global>
%p2 = fly.add_offset(%p1, %o2) : (!fly.ptr<f32, global>, !fly.int_tuple<8>) -> !fly.ptr<f32, global>
return %p2 : !fly.ptr<f32, global>
}
Loading