ScheduleExecution enhancements for timeline-aware scheduling and SCF. (#22483)
Paritioning now handles values used in SCF (and other) region ops
properly and ScheduleExecution now handles timeline-aware ops (those ops
that operate on a timeline and can materialize timepoints even if they
don't naturally have them).
A test op is added to avoid relying on the HAL ops that implement the
interface (I'll probably try to move more tests to test ops/attrs in the
future to keep HAL out of Stream).
Part of #16168 PR sequence (4/6).
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Analysis/BUILD.bazel b/compiler/src/iree/compiler/Dialect/Stream/Analysis/BUILD.bazel
index 3cbb5b5..c26b567 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Analysis/BUILD.bazel
+++ b/compiler/src/iree/compiler/Dialect/Stream/Analysis/BUILD.bazel
@@ -35,6 +35,7 @@
"@llvm-project//llvm:Support",
"@llvm-project//mlir:Analysis",
"@llvm-project//mlir:ArithDialect",
+ "@llvm-project//mlir:ControlFlowInterfaces",
"@llvm-project//mlir:FuncDialect",
"@llvm-project//mlir:FunctionInterfaces",
"@llvm-project//mlir:IR",
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Analysis/CMakeLists.txt b/compiler/src/iree/compiler/Dialect/Stream/Analysis/CMakeLists.txt
index c2dd74c..b229ec2 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Analysis/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Dialect/Stream/Analysis/CMakeLists.txt
@@ -28,6 +28,7 @@
LLVMSupport
MLIRAnalysis
MLIRArithDialect
+ MLIRControlFlowInterfaces
MLIRFuncDialect
MLIRFunctionInterfaces
MLIRIR
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.cpp b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.cpp
index 52a2e66..0bfcf5c 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.cpp
+++ b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.cpp
@@ -10,11 +10,56 @@
#include "llvm/Support/Debug.h"
#include "mlir/IR/AsmState.h"
#include "mlir/IR/PatternMatch.h"
+#include "mlir/Interfaces/ControlFlowInterfaces.h"
#define DEBUG_TYPE "iree-stream-partitioning"
namespace mlir::iree_compiler::IREE::Stream {
+//===----------------------------------------------------------------------===//
+// Utilities
+//===----------------------------------------------------------------------===//
+
+// Collects all values consumed by an operation, including those used in nested
+// regions (e.g. scf.for bodies). This is a conservative analysis that walks all
+// regions to find consumed values.
+void collectConsumedValues(Operation *rootOp,
+ SetVector<Value> &consumedValues) {
+ SmallVector<Operation *> worklist;
+ DenseSet<Operation *> visitedOps;
+ worklist.push_back(rootOp);
+ while (!worklist.empty()) {
+ Operation *op = worklist.pop_back_val();
+
+ // Skip if already visited.
+ if (!visitedOps.insert(op).second) {
+ continue;
+ }
+
+ // Collect direct operands.
+ for (auto operand : op->getOperands()) {
+ consumedValues.insert(operand);
+ }
+
+ // Conservatively walk all regions to find consumed values.
+ // We can't use RegionBranchOpInterface::getEntrySuccessorRegions because
+ // it requires compile-time constant operands to determine reachability,
+ // but we need to handle runtime values. For hazard detection we need to be
+ // conservative and assume all regions may execute.
+ for (auto ®ion : op->getRegions()) {
+ for (auto &block : region) {
+ for (auto &nestedOp : block) {
+ worklist.push_back(&nestedOp);
+ }
+ }
+ }
+ }
+}
+
+//===----------------------------------------------------------------------===//
+// Partition data structures
+//===----------------------------------------------------------------------===//
+
#ifndef NDEBUG
void dumpPartition(Partition &partition, AsmState &asmState) {
@@ -93,6 +138,40 @@
}
}
+ // Check for circular dependencies: input ops using partition outputs.
+ // This can happen if an operation with nested regions (like scf.for) is
+ // incorrectly placed as a partition input when its nested regions actually
+ // consume partition outputs.
+ for (auto in : ins) {
+ // Only check ops, not bare values.
+ auto definingOp = in.getDefiningOp();
+ if (!definingOp)
+ continue;
+
+ // Collect all values used by this input op (including nested regions).
+ SetVector<Value> inputConsumedValues;
+ collectConsumedValues(definingOp, inputConsumedValues);
+
+ // Check if any consumed value comes from partition outputs.
+ //
+ // NOTE: We only check outputs, not all defValues. When operations are
+ // duplicated across partitions (e.g., clones with preferCloneToConsumers),
+ // the same operation may define values in multiple partitions. An input
+ // operation might consume a value that is defined in THIS partition's
+ // defValues, but if that value is not exported (not in outputs), the input
+ // operation must be using a copy from another partition that also defines
+ // it. Checking defValues would create false positives for such cases.
+ for (auto consumedValue : inputConsumedValues) {
+ if (outs.contains(consumedValue)) {
+ return mlir::emitError(loc)
+ << "circular dependency: input operation uses partition output "
+ << consumedValue
+ << " - this indicates the operation should be inside the "
+ "partition";
+ }
+ }
+ }
+
return success();
}
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.h b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.h
index dbf2ba7..923a5a4 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.h
+++ b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning.h
@@ -60,6 +60,15 @@
};
//===----------------------------------------------------------------------===//
+// Utilities
+//===----------------------------------------------------------------------===//
+
+// Collects all values consumed by an operation, including those used in nested
+// regions (e.g. scf.for bodies). Uses a worklist-based approach to avoid stack
+// overflow on deeply nested regions.
+void collectConsumedValues(Operation *rootOp, SetVector<Value> &consumedValues);
+
+//===----------------------------------------------------------------------===//
// Stream partitioning algorithms
//===----------------------------------------------------------------------===//
//
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning/ReferencePartitioning.cpp b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning/ReferencePartitioning.cpp
index e4baa0c..0ae3d0d 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning/ReferencePartitioning.cpp
+++ b/compiler/src/iree/compiler/Dialect/Stream/Analysis/Partitioning/ReferencePartitioning.cpp
@@ -42,6 +42,10 @@
llvm::BitVector membership;
// Which partitions transitively depend on this operation.
llvm::BitVector hazards;
+ // Hazards specifically from nested region captures creating circular
+ // dependencies. These must always be respected, even for operations with
+ // preferCloneToConsumers.
+ llvm::BitVector nestedRegionHazards;
};
struct PartitionBuilder {
@@ -64,9 +68,78 @@
opInfo.hazards.reset(ordinal);
ops.insert(op);
hazards |= opInfo.hazards;
+ hazards |= opInfo.nestedRegionHazards;
}
};
+// Find the effective OpInfo for a user operation.
+// Returns the OpInfo* to use for hazard tracking, or nullptr if the user
+// should be skipped (either because hazards were already set or not found).
+static OpInfo *getEffectiveUserInfo(Operation *user, Block *block,
+ DenseMap<Operation *, OpInfo> &opInfos,
+ OpInfo &opInfo) {
+ // Check if user is in the same block we're partitioning.
+ if (user->getBlock() == block) {
+ // Direct lookup - user in same block.
+ LLVM_DEBUG(llvm::dbgs() << " User in same block\n");
+ auto userInfoIt = opInfos.find(user);
+ if (userInfoIt != opInfos.end()) {
+ return &userInfoIt->second;
+ }
+ return nullptr;
+ }
+
+ LLVM_DEBUG(llvm::dbgs() << " User in different block (nested region)\n");
+
+ // User is in a nested region - find the containing operation in our block.
+ // This handles cases like scf.for where operations inside the loop body
+ // use values from the parent block. We treat the containing operation
+ // (scf.for) as a proxy for all its nested users.
+ Operation *containing = user->getParentOp();
+ while (containing && containing->getBlock() != block) {
+ containing = containing->getParentOp();
+ }
+
+ if (!containing) {
+ return nullptr;
+ }
+
+ // Use the containing op's info as proxy for nested users.
+ auto containingInfoIt = opInfos.find(containing);
+ if (containingInfoIt != opInfos.end()) {
+ // Only use containing op's info if it's streamable.
+ // Non-streamable ops (scf.for, scf.if) need special circular dependency
+ // tracking via nestedRegionHazards to prevent grouping captured values
+ // with operations that consume the containing op's results.
+ if (isa<IREE::Stream::StreamableOpInterface>(containing)) {
+ return &containingInfoIt->second;
+ }
+ // Fall through to set nestedRegionHazards for non-streamable containing
+ // ops.
+ }
+
+ // The containing operation is not streamable (e.g., scf.for).
+ // Values captured into it create circular dependency hazards with any
+ // partitions that use the containing operation's results.
+ for (auto result : containing->getResults()) {
+ for (auto resultUser : result.getUsers()) {
+ if (resultUser->getBlock() == block) {
+ auto resultUserInfoIt = opInfos.find(resultUser);
+ if (resultUserInfoIt != opInfos.end()) {
+ opInfo.nestedRegionHazards |= resultUserInfoIt->second.membership;
+ opInfo.nestedRegionHazards |= resultUserInfoIt->second.hazards;
+ LLVM_DEBUG({
+ llvm::dbgs() << "Setting nestedRegionHazards for captured value, "
+ << "hazard count: "
+ << opInfo.nestedRegionHazards.count() << "\n";
+ });
+ }
+ }
+ }
+ }
+ return nullptr;
+}
+
// This is terrible. See Stream/Analysis/Partition.h for a description of what
// a real implementation would do. We want cost modeling for tie breakers when
// an op could be in multiple partitions, cloning for ops that are not worth
@@ -131,8 +204,31 @@
opHazardsInCandidatePartition |= opInfos[user].hazards;
}
}
+ // For preferCloneToConsumers ops, we DON'T include nestedRegionHazards
+ // because cloning into multiple partitions solves the circular
+ // dependency. The op will be duplicated in each consumer partition, so
+ // there's no shared state causing circular dependencies.
} else {
- opHazards = &opInfo.hazards;
+ // For non-clonable ops, nestedRegionHazards must be respected to prevent
+ // circular dependencies from nested region captures. Unlike normal
+ // hazards which propagate through use-def chains, nestedRegionHazards may
+ // not be fully captured in opInfo.hazards when the containing operation
+ // (e.g., scf.for) is not streamable and returns nullptr from
+ // getEffectiveUserInfo.
+ opHazardsInCandidatePartition = opInfo.hazards;
+ opHazardsInCandidatePartition |= opInfo.nestedRegionHazards;
+ opHazards = &opHazardsInCandidatePartition;
+ }
+
+ // Check nestedRegionHazards separately first. Unlike normal hazards
+ // (which indicate dependencies), nestedRegionHazards indicate partitions
+ // the op CANNOT join due to circular dependency constraints.
+ for (auto nestedHazardOrdinal : opInfo.nestedRegionHazards.set_bits()) {
+ if (nestedHazardOrdinal == partitionOrdinal) {
+ // This op cannot be in this partition due to nested region capture
+ // circular dependency constraints.
+ return false;
+ }
}
for (auto opHazardOrdinal : opHazards->set_bits()) {
@@ -155,8 +251,36 @@
auto asmState = getRootAsmState(block);
- llvm::DenseMap<Operation *, llvm::SmallVector<Operation *>> syncOps;
+ // Build a map from values to operations that consume them in nested regions.
+ // This allows us to detect when an operation with nested regions (like
+ // scf.for) consumes values produced by other operations that may create
+ // hazards even though there's no direct use-def edge.
+ DenseMap<Value, SmallVector<Operation *>> nestedConsumers;
+ for (auto &op : *block) {
+ // Only check operations with regions that are not streamable.
+ // Streamable ops are handled normally but non-streamable ops with regions
+ // (like scf.for, scf.if, scf.while) need special handling.
+ if (op.getNumRegions() == 0 ||
+ isa<IREE::Stream::StreamableOpInterface>(op)) {
+ continue;
+ }
+ // Collect all values consumed by this operation (including in nested
+ // regions).
+ SetVector<Value> consumedValues;
+ collectConsumedValues(&op, consumedValues);
+
+ // For each consumed value record this op as a nested consumer.
+ for (auto value : consumedValues) {
+ // Only track values defined in the same block (not block arguments).
+ auto definingOp = value.getDefiningOp();
+ if (definingOp && definingOp->getBlock() == block) {
+ nestedConsumers[value].push_back(&op);
+ }
+ }
+ }
+
+ llvm::DenseMap<Operation *, llvm::SmallVector<Operation *>> syncOps;
for (auto &op : llvm::reverse(*block)) {
LLVM_DEBUG({
@@ -223,12 +347,16 @@
// We prune the set based on whether the users are part of a transitive
// dependency chain down the use-def chain to a partition.
llvm::BitVector consumers(builders.size(), /*t=*/false);
+
+ // Process direct users (common case).
for (auto user : op.getUsers()) {
- auto userInfoIt = opInfos.find(user);
- if (userInfoIt == opInfos.end()) {
+ OpInfo *effectiveUserInfo =
+ getEffectiveUserInfo(user, block, opInfos, opInfo);
+ if (!effectiveUserInfo) {
continue;
}
- auto &userInfo = userInfoIt->second;
+
+ auto &userInfo = *effectiveUserInfo;
LLVM_DEBUG({
llvm::dbgs() << "Testing user:\n";
user->print(llvm::dbgs(), *asmState);
@@ -245,14 +373,47 @@
opInfo.hazards |= userInfo.hazards;
}
+ // Process indirect users (operations that consume this op's results in
+ // their nested regions e.g. scf.for using a value in its body).
+ for (auto result : op.getResults()) {
+ auto nestedConsumersIt = nestedConsumers.find(result);
+ if (nestedConsumersIt != nestedConsumers.end()) {
+ for (auto *nestedUser : nestedConsumersIt->second) {
+ auto userInfoIt = opInfos.find(nestedUser);
+ if (userInfoIt == opInfos.end()) {
+ continue;
+ }
+ auto &userInfo = userInfoIt->second;
+ LLVM_DEBUG({
+ llvm::dbgs() << "Testing nested region user:\n";
+ nestedUser->print(llvm::dbgs(), *asmState);
+ llvm::dbgs() << "\n";
+ for (auto membershipOrdinal : userInfo.membership.set_bits()) {
+ llvm::dbgs() << " member of partition " << membershipOrdinal
+ << "\n";
+ }
+ for (auto hazardOrdinal : userInfo.hazards.set_bits()) {
+ llvm::dbgs() << " hazard w/ partition " << hazardOrdinal << "\n";
+ }
+ });
+ // For nested consumers we only propagate hazards and not membership.
+ // The nested consumer itself is not streamable and won't be in a
+ // partition but it creates a dependency barrier.
+ opInfo.hazards |= userInfo.membership;
+ opInfo.hazards |= userInfo.hazards;
+ }
+ }
+ }
+
for (auto syncOp : syncOps[&op]) {
for (auto user : syncOp->getUsers()) {
- auto userInfoIt = opInfos.find(user);
- if (userInfoIt == opInfos.end()) {
+ OpInfo *effectiveUserInfo =
+ getEffectiveUserInfo(user, block, opInfos, opInfo);
+ if (!effectiveUserInfo) {
continue;
}
- auto &userInfo = userInfoIt->second;
+ auto &userInfo = *effectiveUserInfo;
LLVM_DEBUG({
llvm::dbgs() << "Testing sync user:\n";
user->print(llvm::dbgs(), *asmState);
@@ -382,8 +543,13 @@
LLVM_DEBUG(llvm::dbgs() << "Moving to first candidate partition "
<< firstCandidateOrdinal << " (continue)\n");
// If we are a clonable op (like splat) clone us into every partition.
+ // We also clone if there are nested region hazards, indicating consumers
+ // in nested regions that will need their own copy.
// Otherwise we just pick the first we find (probably a bad heuristic).
- if (consumers.count() > 1 && streamableOp.preferCloneToConsumers()) {
+ bool shouldClone =
+ streamableOp.preferCloneToConsumers() &&
+ (consumers.count() > 1 || opInfo.nestedRegionHazards.any());
+ if (shouldClone && consumers.any()) {
for (auto consumerOrdinal : consumers.set_bits()) {
LLVM_DEBUG(llvm::dbgs() << "Cloning into consumer partition "
<< consumerOrdinal << "\n");
@@ -391,7 +557,29 @@
consumerBuilder->insert(&op, opInfo);
consumerBuilder->clonedOps.insert(&op);
}
+ } else if (shouldClone && !consumers.any()) {
+ // We want to clone but have no streamable consumers. Check if the op
+ // has any uses in the same block - if so, it needs to be in a partition.
+ // If all uses are in nested regions, leave it for re-materialization.
+ bool hasUsesInSameBlock = false;
+ for (auto result : op.getResults()) {
+ for (auto user : result.getUsers()) {
+ if (user->getBlock() == block) {
+ hasUsesInSameBlock = true;
+ break;
+ }
+ }
+ if (hasUsesInSameBlock) {
+ break;
+ }
+ }
+ if (hasUsesInSameBlock) {
+ // Has uses in same block, insert into first candidate partition.
+ builder->insert(&op, opInfo);
+ }
+ // Else: all uses are in nested regions, leave for re-materialization.
} else {
+ // Not a clonable op, insert into first candidate partition.
builder->insert(&op, opInfo);
}
}
@@ -410,10 +598,17 @@
SetVector<Value> producedValues;
SetVector<Value> escapingValues;
for (auto *op : llvm::reverse(builder->ops)) {
+ // Collect direct operands only. Do NOT recursively collect values from
+ // nested regions of operations that define our operands - that would
+ // incorrectly add values consumed inside control flow ops (scf.for, etc)
+ // to partition inputs, creating spurious circular dependencies.
+ // The circular dependency check in Partition::verify() will correctly
+ // detect actual circular dependencies using collectConsumedValues.
bool didCloneEscape = false;
for (auto operand : op->getOperands()) {
consumedValues.insert(operand);
}
+
for (auto result : op->getResults()) {
producedValues.insert(result);
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp
index fc2bcab..a27ae0b 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.cpp
@@ -4492,6 +4492,31 @@
return getResourceOperandsMutable();
}
+//===----------------------------------------------------------------------===//
+// stream.test.timeline_aware
+//===----------------------------------------------------------------------===//
+
+SmallVector<Value>
+TestTimelineAwareOp::buildAwaitTimepoints(OpBuilder &builder) {
+ // Build timepoint.import ops for each wait fence.
+ SmallVector<Value> timepoints;
+ for (auto fence : getWaitFenceLikes()) {
+ auto importOp = IREE::Stream::TimepointImportOp::create(
+ builder, getLoc(), builder.getType<IREE::Stream::TimepointType>(),
+ ValueRange{fence}, /*affinity=*/nullptr);
+ timepoints.push_back(importOp.getResultTimepoint());
+ }
+ return timepoints;
+}
+
+Value TestTimelineAwareOp::buildResultTimepoint(OpBuilder &builder) {
+ // Build timepoint.import for signal fence.
+ auto importOp = IREE::Stream::TimepointImportOp::create(
+ builder, getLoc(), builder.getType<IREE::Stream::TimepointType>(),
+ ValueRange{getSignalFenceLike()}, /*affinity=*/nullptr);
+ return importOp.getResultTimepoint();
+}
+
} // namespace mlir::iree_compiler::IREE::Stream
//===----------------------------------------------------------------------===//
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td
index 4c9f326..5edc9aa 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamOps.td
@@ -4664,4 +4664,66 @@
} // OpGroupMiscellaneousOps
+//===----------------------------------------------------------------------===//
+// Test ops
+//===----------------------------------------------------------------------===//
+
+def OpGroupTestOps : OpDocGroup {
+ let summary = "Test-only ops for Stream dialect testing";
+ let description = "Ops used only in unit tests.";
+}
+
+let opDocGroup = OpGroupTestOps in {
+
+def Stream_TestTimelineAwareOp : Stream_PureOp<"test.timeline_aware", [
+ AttrSizedOperandSegments,
+ DeclareOpInterfaceMethods<Stream_TimelineAwareOp, [
+ "buildAwaitTimepoints",
+ "buildResultTimepoint",
+ ]>,
+]> {
+ let summary = [{Test op implementing TimelineAwareOpInterface.}];
+ let description = [{
+ Test-only operation that implements TimelineAwareOpInterface directly,
+ allowing Stream dialect tests to verify timeline-aware behavior without
+ requiring HAL/etc dialect dependencies.
+
+ Mimics util.call with the HAL coarse-fences model:
+ - Takes wait fences that must complete before execution.
+ - Takes a signal fence to signal when complete.
+ - Returns an arbitrary result.
+ }];
+
+ let arguments = (ins
+ Variadic<AnyType>:$args,
+ Variadic<Stream_TestFence>:$wait_fence_likes,
+ Stream_TestFence:$signal_fence_like
+ );
+ let results = (outs
+ Variadic<AnyType>:$results
+ );
+
+ let assemblyFormat = [{
+ `(` $args `)` `waits` `(` $wait_fence_likes `)` `signals` `(` $signal_fence_like `)`
+ attr-dict `:` functional-type($args, $results)
+ }];
+
+ let extraClassDeclaration = [{
+ bool participatesInTimeline() { return true; }
+
+ SmallVector<Value> getAwaitFences() {
+ // Return all wait fences (excluding signal fence).
+ SmallVector<Value> fences;
+ for (auto fence : getWaitFenceLikes()) {
+ fences.push_back(fence);
+ }
+ return fences;
+ }
+
+ Value getSignalFence() { return getSignalFenceLike(); }
+ }];
+}
+
+} // OpGroupTestOps
+
#endif // IREE_DIALECT_STREAM_OPS
diff --git a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamTypes.td b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamTypes.td
index 5668a45..1abb9d8 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/IR/StreamTypes.td
+++ b/compiler/src/iree/compiler/Dialect/Stream/IR/StreamTypes.td
@@ -49,6 +49,24 @@
let hasCustomAssemblyFormat = 1;
}
+def Stream_TestFence : Stream_TypeDef<"TestFence", []> {
+ let mnemonic = "test.fence";
+
+ let summary = [{Test-only fence type for timeline-aware op testing.}];
+ let description = [{
+ A test-only type that behaves like hal.fence for testing TimelineAwareOpInterface
+ without introducing HAL dependencies in Stream unit tests.
+ }];
+
+ let parameters = (ins);
+
+ let builders = [
+ TypeBuilder<(ins), [{
+ return $_get($_ctxt);
+ }]>,
+ ];
+}
+
//===----------------------------------------------------------------------===//
// Stream resource value types
//===----------------------------------------------------------------------===//
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleExecution.cpp b/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleExecution.cpp
index ebe8d1b..67e9ab0 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleExecution.cpp
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/ScheduleExecution.cpp
@@ -5,7 +5,6 @@
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/compiler/Dialect/Stream/Analysis/Partitioning.h"
-#include "iree/compiler/Dialect/Stream/IR/StreamDialect.h"
#include "iree/compiler/Dialect/Stream/IR/StreamOps.h"
#include "iree/compiler/Dialect/Stream/IR/StreamTypes.h"
#include "iree/compiler/Dialect/Stream/Transforms/Passes.h"
@@ -224,14 +223,63 @@
return orderedBlocks;
}
+// Returns true if an operation can be placed into a partition (execute region).
+// Only streamable operations should be partitioned; all other operations stay
+// in the parent block and have their nested regions recursively processed.
+//
+// TODO(benvanik): allow limited scf ops inside the regions when we can ensure
+// they rely on only constant values (in the future we can do indirect stuff).
+static bool canPlaceInPartition(Operation *op) {
+ // Only StreamableOpInterface ops can be placed into partitions.
+ // This includes ops like stream.async.slice, stream.async.clone, etc.
+ // but excludes stream.async.execute (which is the partition container itself)
+ // and control flow ops like scf.for.
+ return isa<IREE::Stream::StreamableOpInterface>(op);
+}
+
+// Tries to find a timeline-aware consumer of |value| that can provide a
+// timepoint for synchronization. This looks through stream.tensor.export ops
+// since timeline-aware ops (like util.call with fences) typically work with
+// HAL types rather than stream resources.
+// Returns the timepoint if found and otherwise returns null.
+static Value tryGetConsumerTimepoint(Value value) {
+ SmallVector<Operation *> usersToCheck;
+ usersToCheck.append(value.getUsers().begin(), value.getUsers().end());
+
+ // Look through tensor exports to find timeline-aware ops.
+ for (auto *user : value.getUsers()) {
+ if (auto exportOp = dyn_cast<IREE::Stream::TensorExportOp>(user)) {
+ usersToCheck.append(exportOp.getResult().getUsers().begin(),
+ exportOp.getResult().getUsers().end());
+ }
+ }
+
+ for (auto *user : usersToCheck) {
+ if (auto awareOp = dyn_cast<IREE::Stream::TimelineAwareOpInterface>(user)) {
+ if (awareOp.participatesInTimeline()) {
+ // Let the timeline-aware op build its result timepoint.
+ OpBuilder awareBuilder(user);
+ awareBuilder.setInsertionPointAfter(user);
+ if (auto timepoint = awareOp.buildResultTimepoint(awareBuilder)) {
+ return timepoint;
+ }
+ }
+ }
+ }
+
+ return nullptr;
+}
+
LogicalResult processRegion(Location loc, MLIRContext *context, Region ®ion,
const PartitioningConfigAttr &configAttr) {
for (auto *block : sortBlocksInDominanceOrder(region)) {
// Compute a set of partitions covering all of the streamable ops in the
// block.
auto partitionSet = partitionStreamableOps(configAttr, block);
- if (partitionSet.empty())
+ if (partitionSet.empty()) {
continue;
+ }
+
if (failed(partitionSet.verify(loc))) {
return failure();
}
@@ -275,22 +323,64 @@
for (auto [oldResult, newResult, newResultSize] : llvm::zip_equal(
partitionBuilder.partition->outs, executeOp.getResults(),
executeOp.getResultSizes())) {
- // Insert one await per result. We could batch them all but that would
- // prematurely tie their lifetimes together. By having unique awaits
- // we allow propagation to move the waits further to where the values
- // are used (including right into other execution regions).
- auto awaitOp = IREE::Stream::TimepointAwaitOp::create(
- builder, executeOp.getLoc(), newResult, newResultSize,
- executeOp.getResultTimepoint());
-
- // Explicitly copy the Value since it is marked as const.
- Value toBeDeleted = oldResult;
-
- toBeDeleted.replaceAllUsesWith(awaitOp.getResults().front());
- deadOps.insert(oldResult.getDefiningOp());
+ // Check if any consumer is timeline-aware and can provide its own
+ // timepoint - if so we reuse that to guarantee we don't ever wait.
+ Value deferredTimepoint = tryGetConsumerTimepoint(oldResult);
+ if (deferredTimepoint) {
+ // Timeline-aware op provided a timepoint - use it.
+ auto awaitOp = IREE::Stream::TimepointAwaitOp::create(
+ builder, executeOp.getLoc(), newResult, newResultSize,
+ deferredTimepoint);
+ deadOps.insert(oldResult.getDefiningOp());
+ Value toReplace = oldResult;
+ toReplace.replaceAllUsesWith(awaitOp.getResults().front());
+ } else {
+ // Normal case - immediate await on the execute's timepoint.
+ auto awaitOp = IREE::Stream::TimepointAwaitOp::create(
+ builder, executeOp.getLoc(), newResult, newResultSize,
+ executeOp.getResultTimepoint());
+ deadOps.insert(oldResult.getDefiningOp());
+ Value toReplace = oldResult;
+ toReplace.replaceAllUsesWith(awaitOp.getResults().front());
+ }
}
}
+ // Before erasing preferCloneToConsumers ops, re-materialize them in nested
+ // regions that reference them.
+ DenseMap<Operation *, Operation *> rematerializedOps;
+ for (auto *deadOp : deadOps) {
+ auto streamableOp = dyn_cast<IREE::Stream::StreamableOpInterface>(deadOp);
+ if (!streamableOp || !streamableOp.preferCloneToConsumers()) {
+ continue;
+ }
+
+ // Find all uses in nested regions and re-materialize.
+ for (auto &use : llvm::make_early_inc_range(deadOp->getUses())) {
+ Operation *user = use.getOwner();
+ // Only look at users in other regions.
+ if (user->getBlock() == deadOp->getBlock()) {
+ continue;
+ }
+
+ // Re-materialize the op at the start of the user's block.
+ Block *nestedBlock = user->getBlock();
+ auto *clonedOp = deadOp->clone();
+ nestedBlock->getOperations().insert(nestedBlock->begin(), clonedOp);
+
+ // Replace this use with the cloned op's result.
+ Value oldValue = use.get();
+ auto resultValue = dyn_cast<OpResult>(oldValue);
+ unsigned resultIndex = resultValue.getResultNumber();
+ use.set(clonedOp->getResult(resultIndex));
+ }
+ }
+
for (auto *deadOp : llvm::reverse(deadOps)) {
+ if (!deadOp->use_empty()) {
+ // Keep ops that are still used (shouldn't happen after
+ // re-materialization).
+ continue;
+ }
deadOp->erase();
}
@@ -304,9 +394,27 @@
});
}
+ // Process nested regions AFTER partitioning the current block.
+ // This allows nested regions to use the remapped values from parent
+ // partitions. We only process operations that were NOT placed into
+ // partitions above (e.g., control flow operations like scf.for).
for (auto *block : sortBlocksInDominanceOrder(region)) {
for (auto &op : *block) {
- if (isa<scf::SCFDialect>(op.getDialect())) {
+ // Skip ops that were placed into partitions (they're being moved).
+ if (canPlaceInPartition(&op)) {
+ continue;
+ }
+
+ // Process any op with regions that supports control flow, except for
+ // stream.async.execute and stream.async.concurrent which are the
+ // partition containers created by this pass.
+ if (isa<RegionBranchOpInterface>(op)) {
+ // Don't recurse into the partition containers we just created.
+ if (isa<IREE::Stream::AsyncExecuteOp>(op) ||
+ isa<IREE::Stream::AsyncConcurrentOp>(op)) {
+ continue;
+ }
+
for (auto &subregion : op.getRegions()) {
if (failed(processRegion(loc, context, subregion, configAttr)))
return failure();
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/BUILD.bazel b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/BUILD.bazel
index 4817ff2..a68e003 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/BUILD.bazel
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/BUILD.bazel
@@ -55,6 +55,8 @@
"schedule_allocation.mlir",
"schedule_concurrency.mlir",
"schedule_execution.mlir",
+ "schedule_execution_scf.mlir",
+ "schedule_execution_timeline_aware.mlir",
"specialize_dispatches.mlir",
"specialize_encodings.mlir",
"sync_initializers.mlir",
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/CMakeLists.txt b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/CMakeLists.txt
index 658bcc6..83329a5 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/CMakeLists.txt
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/CMakeLists.txt
@@ -53,6 +53,8 @@
"schedule_allocation.mlir"
"schedule_concurrency.mlir"
"schedule_execution.mlir"
+ "schedule_execution_scf.mlir"
+ "schedule_execution_timeline_aware.mlir"
"specialize_dispatches.mlir"
"specialize_encodings.mlir"
"sync_initializers.mlir"
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution.mlir b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution.mlir
index 0220467..f2786a2 100644
--- a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution.mlir
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution.mlir
@@ -514,3 +514,50 @@
}
util.return %sum, %arg1 : !stream.resource<*>, index
}
+
+// -----
+
+// Tests clones that span multiple partitions separated by a load barrier do not
+// create a circular dependency when the clone is used on both sides of the
+// barrier. The clone is duplicated into both partitions.
+
+// CHECK-LABEL: @cloneAcrossLoadBarrier
+util.func public @cloneAcrossLoadBarrier(%arg0: !stream.resource<external>) -> (!stream.resource<transient>, !stream.resource<transient>) {
+ %c0 = arith.constant 0 : index
+ %c8 = arith.constant 8 : index
+ %c16 = arith.constant 16 : index
+ %c24 = arith.constant 24 : index
+
+ // First partition: clone + dispatch0 + transfer (before load barrier).
+ // CHECK: %[[RESULTS0:.+]]:2, %[[TP0:.+]] = stream.async.execute
+ // CHECK-SAME: with({{.+}} as %[[ARG0:.+]]: !stream.resource<external>{%c24})
+ // CHECK-SAME: -> (!stream.resource<transient>{%c16}, !stream.resource<staging>{%c16})
+ // CHECK-NEXT: %[[CLONE0:.+]] = stream.async.clone %[[ARG0]]
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c24} -> !stream.resource<external>{%c24}
+ // CHECK-NEXT: %[[DISPATCH0:.+]] = stream.async.dispatch @ex::@dispatch_0(%[[CLONE0]]
+ %dispatch0 = stream.async.dispatch @ex::@dispatch_0(%clone[%c0 to %c24 for %c24]) : (!stream.resource<external>{%c24}) -> !stream.resource<transient>{%c16}
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[DISPATCH0]]
+ %transfer = stream.async.transfer %dispatch0 : !stream.resource<transient>{%c16} -> !stream.resource<staging>{%c16}
+ // CHECK-NEXT: stream.yield %[[DISPATCH0]], %[[TRANSFER]]
+
+ // CHECK: stream.timepoint.await %[[TP0]] => %[[RESULTS0]]#1
+ // CHECK: %[[LOADED:.+]] = stream.async.load
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c16} -> i64
+ // Second partition: clone (duplicated) + splat + dispatch1 + dispatch2 (after load barrier).
+ // CHECK: %[[RESULTS1:.+]]:2, %[[TP1:.+]] = stream.async.execute
+ // CHECK-SAME: await(%[[TP0]])
+ // CHECK-SAME: with({{.+}} as %[[ARG1:.+]]: !stream.resource<external>{%c24},
+ // CHECK-SAME: %[[RESULTS0]]#0 as %[[DISPATCH0_IN:.+]]: !stream.resource<transient>{%c16})
+ // CHECK-SAME: -> (!stream.resource<transient>{%c8}, !stream.resource<transient>{%c8})
+ // CHECK-NEXT: %[[CLONE1:.+]] = stream.async.clone %[[ARG1]]
+ // CHECK-NEXT: %[[SPLAT:.+]] = stream.async.splat %[[LOADED]]
+ %filled = stream.async.splat %loaded : i64 -> !stream.resource<transient>{%c24}
+ // CHECK-NEXT: %[[DISPATCH1:.+]] = stream.async.dispatch @ex::@dispatch_1(%[[DISPATCH0_IN]]{{.+}}, %[[SPLAT]]
+ %dispatch1 = stream.async.dispatch @ex::@dispatch_1(%dispatch0[%c0 to %c16 for %c16], %filled[%c0 to %c24 for %c24]) : (!stream.resource<transient>{%c16}, !stream.resource<transient>{%c24}) -> !stream.resource<transient>{%c8}
+ // CHECK-NEXT: %[[DISPATCH2:.+]] = stream.async.dispatch @ex::@dispatch_2(%[[CLONE1]]
+ %dispatch2 = stream.async.dispatch @ex::@dispatch_2(%clone[%c0 to %c24 for %c24]) : (!stream.resource<external>{%c24}) -> !stream.resource<transient>{%c8}
+ // CHECK-NEXT: stream.yield %[[DISPATCH1]], %[[DISPATCH2]]
+
+ // CHECK: stream.timepoint.await %[[TP1]] => %[[RESULTS1]]
+ util.return %dispatch1, %dispatch2 : !stream.resource<transient>, !stream.resource<transient>
+}
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_scf.mlir b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_scf.mlir
new file mode 100644
index 0000000..5a4dbf8
--- /dev/null
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_scf.mlir
@@ -0,0 +1,1147 @@
+// RUN: iree-opt --split-input-file --allow-unregistered-dialect --pass-pipeline="builtin.module(util.func(iree-stream-schedule-execution))" %s | FileCheck %s
+
+// Tests basic scf.for loop with dependency on cloned resource (reproducer case).
+// The loop uses a cloned resource from a partition, which creates a dependency
+// that must be respected during partitioning.
+
+// CHECK-LABEL: @scfForWithDependency
+util.func public @scfForWithDependency(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c11 = arith.constant 11 : index
+ %c48 = arith.constant 48 : index
+ %c0_i64 = arith.constant 0 : i64
+ %c5_i64 = arith.constant 5 : i64
+
+ // CHECK: scf.for
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c48} -> !stream.resource<external>{%c48}
+
+
+ %result = scf.for %i = %c0 to %c11 step %c1 iter_args(%iter = %c0_i64) -> (i64) {
+ %offset = arith.muli %i, %c4 : index
+ %end = arith.addi %offset, %c4 : index
+ // Loop uses the cloned resource - this should NOT be partitioned into the clone partition.
+ // CHECK: %[[EXEC_RESULT:.+]], %[[EXEC_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[CAPTURE:.+]]: !stream.resource<external>{%c48})
+ // CHECK-SAME: -> !stream.resource<staging>{%c4}
+ // CHECK-NEXT: %[[CLONE:.+]] = stream.async.clone %[[CAPTURE]]
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[CLONE]]
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[SLICE]]
+ // CHECK-NEXT: stream.yield %[[TRANSFER]]
+ %slice = stream.async.slice %clone[%offset to %end] : !stream.resource<external>{%c48} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: stream.timepoint.await %[[EXEC_TIMEPOINT]]
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ %ext = arith.extsi %load : i32 to i64
+ %cmp = arith.cmpi eq, %ext, %c5_i64 : i64
+ %add = arith.extui %cmp : i1 to i64
+ %next = arith.addi %iter, %add : i64
+ scf.yield %next : i64
+ }
+
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %result : i64 -> !stream.resource<external>{%c4}
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.for loop result being used by a dispatch.
+// The loop produces a value that is consumed by a partition.
+
+stream.async.func private @dispatch(%arg0: i64, %arg1: !stream.resource<*>) -> !stream.resource<*>
+
+// CHECK-LABEL: @scfForProducingDispatchInput
+util.func public @scfForProducingDispatchInput(%arg0: !stream.resource<*>) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c10 = arith.constant 10 : index
+ %c128 = arith.constant 128 : index
+ %c0_i64 = arith.constant 0 : i64
+ %c1_i64 = arith.constant 1 : i64
+
+ // CHECK: scf.for
+ %count = scf.for %i = %c0 to %c10 step %c1 iter_args(%iter = %c0_i64) -> (i64) {
+ %next = arith.addi %iter, %c1_i64 : i64
+ scf.yield %next : i64
+ }
+
+ // The dispatch should be in a partition that waits for the loop to complete.
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.call @dispatch
+ %result = stream.async.call @dispatch(%count, %arg0[%c0 to %c128 for %c128]) : (i64, !stream.resource<*>{%c128}) -> %arg0{%c128}
+ // CHECK: stream.timepoint.await
+ util.return %result : !stream.resource<*>
+}
+
+// -----
+
+// Tests nested scf.for loops with stream operations.
+// Both inner and outer loops should remain outside partitions.
+
+// CHECK-LABEL: @scfNestedFor
+util.func public @scfNestedFor(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c3 = arith.constant 3 : index
+ %c4 = arith.constant 4 : index
+ %c16 = arith.constant 16 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // CHECK: scf.for
+ %outer_result = scf.for %i = %c0 to %c3 step %c1 iter_args(%outer_iter = %c0_i32) -> (i32) {
+ // CHECK: scf.for
+ %inner_result = scf.for %j = %c0 to %c3 step %c1 iter_args(%inner_iter = %outer_iter) -> (i32) {
+ %offset = arith.muli %j, %c4 : index
+ %end = arith.addi %offset, %c4 : index
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.slice
+ // CHECK-NEXT: stream.async.transfer
+ %slice = stream.async.slice %arg0[%offset to %end] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: stream.timepoint.await
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ %add = arith.addi %inner_iter, %load : i32
+ scf.yield %add : i32
+ }
+ scf.yield %inner_result : i32
+ }
+
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %outer_result : i32 -> !stream.resource<external>{%c4}
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.for loop that uses multiple values produced by a single partition
+// before the loop. The key expectations:
+// 1. The two slices are created in a single execute region before the loop.
+// 2. The loop does NOT await the timepoint before entering.
+// 3. Inside the loop body, both slices are used in a single execute region
+// that awaits the partition's timepoint.
+// 4. The two transfers are merged into one execute region.
+
+// CHECK-LABEL: @scfForWithMultipleRegionUses
+util.func public @scfForWithMultipleRegionUses(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c16 = arith.constant 16 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // CHECK: %[[RESULTS:.+]]:2, %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[CAPTURE:.+]]: !stream.resource<external>{%c16})
+ // CHECK-SAME: -> (!stream.resource<external>{%c8}, !stream.resource<external>{%c8})
+ // CHECK-NEXT: %[[SLICE0:.+]] = stream.async.slice %[[CAPTURE]][%c0 to %c8]
+ %slice0 = stream.async.slice %arg0[%c0 to %c8] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[SLICE1:.+]] = stream.async.slice %[[CAPTURE]][%c8 to %c16]
+ %slice1 = stream.async.slice %arg0[%c8 to %c16] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: stream.yield %[[SLICE0]], %[[SLICE1]]
+
+ // CHECK-NOT: stream.timepoint.await
+ // CHECK: scf.for
+ %result = scf.for %i = %c0 to %c4 step %c1 iter_args(%iter = %c0_i32) -> (i32) {
+ // Loop uses both slices from the partition. Both transfers merged into one execute.
+ // CHECK: stream.async.execute await(%[[TIMEPOINT]])
+ // CHECK-NEXT: stream.async.transfer
+ %transfer0 = stream.async.transfer %slice0 : !stream.resource<external>{%c8} -> !stream.resource<staging>{%c8}
+ // CHECK-NEXT: stream.async.transfer
+ %transfer1 = stream.async.transfer %slice1 : !stream.resource<external>{%c8} -> !stream.resource<staging>{%c8}
+ // CHECK: stream.timepoint.await
+ %load0 = stream.async.load %transfer0[%c0] : !stream.resource<staging>{%c8} -> i32
+
+ // CHECK-NOT: stream.async.execute
+ // CHECK-NOT: stream.timepoint.await
+ %load1 = stream.async.load %transfer1[%c0] : !stream.resource<staging>{%c8} -> i32
+
+ %add0 = arith.addi %iter, %load0 : i32
+ %add1 = arith.addi %add0, %load1 : i32
+ scf.yield %add1 : i32
+ }
+
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %result : i32 -> !stream.resource<external>{%c4}
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.for loop with zero iterations.
+// The loop may not execute at all, which affects partitioning.
+
+// CHECK-LABEL: @scfForZeroIterations
+util.func public @scfForZeroIterations(%arg0: !stream.resource<external>, %lb: index, %ub: index) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c16 = arith.constant 16 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // Clone is NOT materialized outside the loop since the loop may not execute.
+ // CHECK-NOT: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16} -> !stream.resource<external>{%c16}
+
+ // CHECK: scf.for
+ %result = scf.for %i = %lb to %ub step %c1 iter_args(%iter = %c0_i32) -> (i32) {
+ // Clone is materialized INSIDE the execute region within the loop body.
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.clone
+ // CHECK-NEXT: stream.async.slice
+ // CHECK-NEXT: stream.async.transfer
+ %slice = stream.async.slice %clone[%c0 to %c4] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: stream.timepoint.await
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ %add = arith.addi %iter, %load : i32
+ scf.yield %add : i32
+ }
+
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %result : i32 -> !stream.resource<external>{%c4}
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.if with both then and else branches containing stream operations.
+// Both branches should be partitioned independently.
+
+// CHECK-LABEL: @scfIfThenElse
+util.func public @scfIfThenElse(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c16 = arith.constant 16 : index
+
+ // CHECK: scf.if
+ %result = scf.if %cond -> !stream.resource<external> {
+ // CHECK: %[[RESULTS_0:.+]], %[[RESULT_TIMEPOINT_1:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG2:.+]]: !stream.resource<external>{%c16})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG2]][%c0 to %c8]
+ %slice = stream.async.slice %arg0[%c0 to %c8] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: stream.yield %[[SLICE]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT_1]] => %[[RESULTS_0]]
+ // CHECK: scf.yield %[[AWAITED]]
+ scf.yield %slice : !stream.resource<external>
+ } else {
+ // CHECK: %[[RESULTS_0:.+]], %[[RESULT_TIMEPOINT_1:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG2:.+]]: !stream.resource<external>{%c16})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG2]][%c8 to %c16]
+ %slice = stream.async.slice %arg0[%c8 to %c16] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: stream.yield %[[SLICE]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT_1]] => %[[RESULTS_0]]
+ // CHECK: scf.yield %[[AWAITED]]
+ scf.yield %slice : !stream.resource<external>
+ }
+
+ // CHECK: %[[RESULTS:.+]], %[[RESULT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG2:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[CLONE:.+]] = stream.async.clone %[[ARG2]]
+ %transfer = stream.async.transfer %result : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: stream.yield %[[CLONE]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT]] => %[[RESULTS]]
+ // CHECK: util.return %[[AWAITED]]
+ util.return %transfer : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.if with only then branch (no else).
+// The then branch contains stream operations.
+
+// CHECK-LABEL: @scfIfThenOnly
+util.func public @scfIfThenOnly(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+
+ // CHECK: scf.if
+ %result = scf.if %cond -> !stream.resource<external> {
+ // CHECK: %[[RESULTS:.+]], %[[RESULT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG2:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG2]][%c0 to %c4]
+ %slice = stream.async.slice %arg0[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: stream.yield %[[SLICE]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT]] => %[[RESULTS]]
+ // CHECK: scf.yield %[[AWAITED]]
+ scf.yield %slice : !stream.resource<external>
+ } else {
+ scf.yield %arg0 : !stream.resource<external>
+ }
+
+ // CHECK: util.return
+ util.return %result : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.if using cloned resource from partition.
+// The if operation depends on a partition output.
+
+// CHECK-LABEL: @scfIfWithDependency
+util.func public @scfIfWithDependency(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+
+ // CHECK: %[[RESULTS:.+]], %[[RESULT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG2:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[CLONE_OP:.+]] = stream.async.clone %[[ARG2]]
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: stream.yield %[[CLONE_OP]]
+
+ // The else branch uses clone directly (not in a stream op), so await must happen before scf.if.
+ // CHECK: %[[CLONE_READY:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT]] => %[[RESULTS]]
+ // CHECK: scf.if
+ %result = scf.if %cond -> !stream.resource<external> {
+ // Then branch uses cloned resource in stream op - can reference original clone.
+ // CHECK: %[[RESULTS_0:.+]], %[[RESULT_TIMEPOINT_1:.+]] = stream.async.execute await(%[[RESULT_TIMEPOINT]])
+ // CHECK-SAME: with(%[[RESULTS]] as %[[ARG2:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG2]][%c0 to %c4]
+ %slice = stream.async.slice %clone[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: stream.yield %[[SLICE]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT_1]] => %[[RESULTS_0]]
+ // CHECK: scf.yield %[[AWAITED]]
+ scf.yield %slice : !stream.resource<external>
+ } else {
+ // CHECK: scf.yield %[[CLONE_READY]]
+ scf.yield %clone : !stream.resource<external>
+ }
+
+ // CHECK: util.return
+ util.return %result : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.if result being used by a dispatch.
+// The if produces a value consumed by a partition.
+
+stream.async.func private @dispatch(%arg0: !stream.resource<*>, %arg1: index) -> %arg0
+
+// CHECK-LABEL: @scfIfProducingDispatchInput
+util.func public @scfIfProducingDispatchInput(%cond: i1, %arg0: !stream.resource<*>) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c128 = arith.constant 128 : index
+ %c256 = arith.constant 256 : index
+
+ %size = scf.if %cond -> index {
+ // To prevent folding to arith.select.
+ "some.sideeffect"() : () -> ()
+ scf.yield %c128 : index
+ } else {
+ scf.yield %c256 : index
+ }
+
+ // The dispatch should be in a partition.
+ // CHECK: %[[RESULTS:.+]], %[[RESULT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-NEXT: stream.async.call @dispatch
+ %result = stream.async.call @dispatch(%arg0[%c0 to %size for %size], %size) : (!stream.resource<*>{%size}, index) -> %arg0{%size}
+ // CHECK: stream.timepoint.await %[[RESULT_TIMEPOINT]] => %[[RESULTS]]
+ util.return %result : !stream.resource<*>
+}
+
+// -----
+
+// Tests scf.if nested inside scf.for with stream operations.
+// Both control flow operations should remain outside partitions.
+
+// CHECK-LABEL: @scfNestedIfInFor
+util.func public @scfNestedIfInFor(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c16 = arith.constant 16 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // CHECK: scf.for
+ %result = scf.for %i = %c0 to %c4 step %c1 iter_args(%iter = %c0_i32) -> (i32) {
+ %c2 = arith.constant 2 : index
+ %rem = arith.remui %i, %c2 : index
+ %cond = arith.cmpi eq, %rem, %c0 : index
+
+ // CHECK: scf.if
+ %value = scf.if %cond -> i32 {
+ // CHECK: %[[RESULTS_0:.+]], %[[RESULT_TIMEPOINT_1:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG3:.+]]: !stream.resource<external>{%c16})
+ // CHECK-SAME: -> !stream.resource<staging>{%c4}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG3]][%c0 to %c4]
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[SLICE]]
+ %slice = stream.async.slice %arg0[%c0 to %c4] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK-NEXT: stream.yield %[[TRANSFER]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT_1]] => %[[RESULTS_0]]
+ // CHECK: %[[LOAD:.+]] = stream.async.load %[[AWAITED]][%c0]
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ // CHECK: scf.yield %[[LOAD]]
+ scf.yield %load : i32
+ } else {
+ // CHECK: %[[RESULTS_0:.+]], %[[RESULT_TIMEPOINT_1:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG3:.+]]: !stream.resource<external>{%c16})
+ // CHECK-SAME: -> !stream.resource<staging>{%c8}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[ARG3]][%c8 to %c16]
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[SLICE]]
+ %slice = stream.async.slice %arg0[%c8 to %c16] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c8} -> !stream.resource<staging>{%c8}
+ // CHECK-NEXT: stream.yield %[[TRANSFER]]
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[RESULT_TIMEPOINT_1]] => %[[RESULTS_0]]
+ // CHECK: %[[LOAD:.+]] = stream.async.load %[[AWAITED]][%c0]
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c8} -> i32
+ // CHECK: scf.yield %[[LOAD]]
+ scf.yield %load : i32
+ }
+
+ %add = arith.addi %iter, %value : i32
+ scf.yield %add : i32
+ }
+
+ // CHECK: %[[RESULTS:.+]], %[[RESULT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %result : i32 -> !stream.resource<external>{%c4}
+ // CHECK: stream.timepoint.await %[[RESULT_TIMEPOINT]] => %[[RESULTS]]
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests basic scf.while loop with stream operations in before/after regions.
+// Both regions should be partitioned independently.
+
+// CHECK-LABEL: @scfWhileBasic
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<external>)
+util.func public @scfWhileBasic(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c10 = arith.constant 10 : index
+ %c0_i32 = arith.constant 0 : i32
+ %c100_i32 = arith.constant 100 : i32
+
+ // CHECK: %[[WHILE_RESULTS:.+]]:2 = scf.while
+ // CHECK-SAME: (%[[ARG1:.+]] = %c0_i32, %[[ARG2:.+]] = %c0)
+ // CHECK-SAME: (i32, index) -> (i32, index)
+ %result:2 = scf.while (%iter = %c0_i32, %count = %c0) : (i32, index) -> (i32, index) {
+ %cond = arith.cmpi slt, %count, %c10 : index
+ // CHECK: scf.condition
+ scf.condition(%cond) %iter, %count : i32, index
+ } do {
+ ^bb0(%iter: i32, %count: index):
+ // CHECK: ^bb0(%[[ITER:.+]]: i32, %[[COUNT:.+]]: index):
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]] as %[[CAPTURE:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<staging>{%c4}
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[CAPTURE]][%c0 to %c4]
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[SLICE]]
+ // CHECK-NEXT: stream.yield %[[TRANSFER]]
+ %slice = stream.async.slice %arg0[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: %[[READY:.+]] = stream.timepoint.await %[[TIMEPOINT]] => %[[RESULTS]]
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ // CHECK: %[[LOAD:.+]] = stream.async.load %[[READY]][%c0]
+ %add = arith.addi %iter, %load : i32
+ %next_count = arith.addi %count, %c1 : index
+ scf.yield %add, %next_count : i32, index
+ }
+
+ // CHECK: %[[SPLAT_RESULTS:.+]], %[[SPLAT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: %[[SPLAT:.+]] = stream.async.splat %[[WHILE_RESULTS]]#0
+ // CHECK-NEXT: stream.yield %[[SPLAT]]
+ %splat = stream.async.splat %result#0 : i32 -> !stream.resource<external>{%c4}
+ // CHECK: %[[FINAL:.+]] = stream.timepoint.await %[[SPLAT_TIMEPOINT]] => %[[SPLAT_RESULTS]]
+ // CHECK: util.return %[[FINAL]]
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.while loop using cloned resource from partition.
+// The while depends on a partition output.
+
+// CHECK-LABEL: @scfWhileWithDependency
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<external>)
+util.func public @scfWhileWithDependency(%arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c5 = arith.constant 5 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+
+ // CHECK: %[[WHILE_RESULTS:.+]]:2 = scf.while
+ // CHECK-SAME: (%[[ARG1:.+]] = %c0_i32, %[[ARG2:.+]] = %c0)
+ // CHECK-SAME: (i32, index) -> (i32, index)
+ %result:2 = scf.while (%iter = %c0_i32, %count = %c0) : (i32, index) -> (i32, index) {
+ %cond = arith.cmpi slt, %count, %c5 : index
+ scf.condition(%cond) %iter, %count : i32, index
+ } do {
+ ^bb0(%iter: i32, %count: index):
+ // CHECK: ^bb0(%[[ITER:.+]]: i32, %[[COUNT:.+]]: index):
+ // While uses the cloned resource.
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]] as %[[CAPTURE:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<staging>{%c4}
+ // CHECK-NEXT: %[[CLONE:.+]] = stream.async.clone %[[CAPTURE]]
+ // CHECK-NEXT: %[[SLICE:.+]] = stream.async.slice %[[CLONE]][%c0 to %c4]
+ // CHECK-NEXT: %[[TRANSFER:.+]] = stream.async.transfer %[[SLICE]]
+ // CHECK-NEXT: stream.yield %[[TRANSFER]]
+ %slice = stream.async.slice %clone[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: %[[READY:.+]] = stream.timepoint.await %[[TIMEPOINT]] => %[[RESULTS]]
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ // CHECK: %[[LOAD:.+]] = stream.async.load %[[READY]][%c0]
+ %add = arith.addi %iter, %load : i32
+ %next_count = arith.addi %count, %c1 : index
+ scf.yield %add, %next_count : i32, index
+ }
+
+ // CHECK: %[[SPLAT_RESULTS:.+]], %[[SPLAT_TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: %[[SPLAT:.+]] = stream.async.splat %[[WHILE_RESULTS]]#0
+ // CHECK-NEXT: stream.yield %[[SPLAT]]
+ %splat = stream.async.splat %result#0 : i32 -> !stream.resource<external>{%c4}
+ // CHECK: %[[FINAL:.+]] = stream.timepoint.await %[[SPLAT_TIMEPOINT]] => %[[SPLAT_RESULTS]]
+ // CHECK: util.return %[[FINAL]]
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests scf.while result being used by a dispatch.
+// The while produces a value consumed by a partition.
+
+stream.async.func private @dispatch(%arg0: i64, %arg1: !stream.resource<*>) -> !stream.resource<*>
+
+// CHECK-LABEL: @scfWhileProducingDispatchInput
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<*>)
+util.func public @scfWhileProducingDispatchInput(%arg0: !stream.resource<*>) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c10 = arith.constant 10 : index
+ %c128 = arith.constant 128 : index
+ %c0_i64 = arith.constant 0 : i64
+ %c1_i64 = arith.constant 1 : i64
+
+ // CHECK: %[[WHILE_RESULTS:.+]]:2 = scf.while
+ // CHECK-SAME: (%[[ARG1:.+]] = %c0_i64, %[[ARG2:.+]] = %c0)
+ // CHECK-SAME: (i64, index) -> (i64, index)
+ %count:2 = scf.while (%iter = %c0_i64, %idx = %c0) : (i64, index) -> (i64, index) {
+ %cond = arith.cmpi slt, %idx, %c10 : index
+ scf.condition(%cond) %iter, %idx : i64, index
+ } do {
+ ^bb0(%iter: i64, %idx: index):
+ %next = arith.addi %iter, %c1_i64 : i64
+ %next_idx = arith.addi %idx, %c1 : index
+ scf.yield %next, %next_idx : i64, index
+ }
+
+ // The dispatch should be in a partition.
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]] as %[[CAPTURE:.+]]: !stream.resource<*>{%c128})
+ // CHECK-SAME: -> {{.+}}{%c128}
+ // CHECK-NEXT: %[[CALL:.+]] = stream.async.call @dispatch(%[[WHILE_RESULTS]]#0, %[[CAPTURE]][%c0 to %c128 for %c128])
+ // CHECK-NEXT: stream.yield %[[CALL]]
+ %result = stream.async.call @dispatch(%count#0, %arg0[%c0 to %c128 for %c128]) : (i64, !stream.resource<*>{%c128}) -> %arg0{%c128}
+ // CHECK: %[[FINAL:.+]] = stream.timepoint.await %[[TIMEPOINT]] => %[[RESULTS]]
+ // CHECK: util.return %[[FINAL]]
+ util.return %result : !stream.resource<*>
+}
+
+// -----
+
+// Tests combination of scf.if, scf.for, and scf.while with stream operations.
+// All control flow operations should remain outside partitions.
+
+// CHECK-LABEL: @scfMixedControlFlow
+util.func public @scfMixedControlFlow(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c16 = arith.constant 16 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // CHECK-NOT: stream.async.execute
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16} -> !stream.resource<external>{%c16}
+
+ // CHECK-NOT: stream.timepoint.await
+ // CHECK: scf.if
+ %if_result = scf.if %cond -> i32 {
+ // CHECK: scf.for
+ %for_result = scf.for %i = %c0 to %c2 step %c1 iter_args(%iter = %c0_i32) -> (i32) {
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.clone
+ // CHECK-NEXT: stream.async.slice
+ // CHECK-NEXT: stream.async.transfer
+ %slice = stream.async.slice %clone[%c0 to %c4] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ // CHECK: stream.timepoint.await
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ %add = arith.addi %iter, %load : i32
+ scf.yield %add : i32
+ }
+ scf.yield %for_result : i32
+ } else {
+ // CHECK: scf.while
+ %while_result:2 = scf.while (%iter = %c0_i32, %count = %c0) : (i32, index) -> (i32, index) {
+ %cond_while = arith.cmpi slt, %count, %c2 : index
+ scf.condition(%cond_while) %iter, %count : i32, index
+ } do {
+ ^bb0(%iter: i32, %count: index):
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.clone
+ // CHECK-NEXT: stream.async.slice
+ // CHECK-NEXT: stream.async.transfer
+ %slice = stream.async.slice %clone[%c8 to %c16] : !stream.resource<external>{%c16} -> !stream.resource<external>{%c8}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c8} -> !stream.resource<staging>{%c8}
+ // CHECK: stream.timepoint.await
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c8} -> i32
+ %add = arith.addi %iter, %load : i32
+ %next_count = arith.addi %count, %c1 : index
+ scf.yield %add, %next_count : i32, index
+ }
+ scf.yield %while_result#0 : i32
+ }
+
+ // CHECK: stream.async.execute
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat %if_result : i32 -> !stream.resource<external>{%c4}
+ // CHECK: stream.timepoint.await
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests control flow operations across multiple partitions.
+// Stream operations before and after control flow should form separate partitions.
+
+stream.async.func private @dispatch0(%arg0: !stream.resource<*>, %arg1: index) -> %arg0
+stream.async.func private @dispatch1(%arg0: !stream.resource<*>, %arg1: index) -> %arg0
+
+// CHECK-LABEL: @scfControlFlowAcrossPartitions
+util.func public @scfControlFlowAcrossPartitions(%cond: i1, %arg0: !stream.resource<*>, %loop_bound: index) -> (!stream.resource<*>, i32) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c128 = arith.constant 128 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // First partition before control flow.
+ // CHECK: %[[RESULTS0:.+]], %[[TIMEPOINT0:.+]] = stream.async.execute
+ // CHECK-NEXT: stream.async.call @dispatch0
+ %result0 = stream.async.call @dispatch0(%arg0[%c0 to %c128 for %c128], %c128) : (!stream.resource<*>{%c128}, index) -> %arg0{%c128}
+
+ // Control flow in the middle - loop that loads data.
+ // CHECK-NOT: stream.timepoint.await
+ // CHECK: scf.for
+ %loop_result:2 = scf.for %i = %c0 to %loop_bound step %c1 iter_args(%iter = %result0, %sum = %c0_i32) -> (!stream.resource<*>, i32) {
+ // CHECK: stream.async.execute await(%[[TIMEPOINT0]])
+ // CHECK-NEXT: stream.async.transfer
+ %transfer = stream.async.transfer %iter : !stream.resource<*>{%c128} -> !stream.resource<staging>{%c128}
+ // CHECK: stream.timepoint.await
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c128} -> i32
+ %next_sum = arith.addi %sum, %loaded : i32
+ scf.yield %iter, %next_sum : !stream.resource<*>, i32
+ }
+
+ // Second partition after control flow.
+ // CHECK: stream.async.execute await(%[[TIMEPOINT0]])
+ // CHECK-NEXT: stream.async.call @dispatch1
+ %result1 = stream.async.call @dispatch1(%loop_result#0[%c0 to %c128 for %c128], %c128) : (!stream.resource<*>{%c128}, index) -> %loop_result#0{%c128}
+ // CHECK: stream.timepoint.await
+ util.return %result1, %loop_result#1 : !stream.resource<*>, i32
+}
+
+// -----
+
+// Tests control flow with device affinities.
+// Partitions should respect affinity boundaries with control flow.
+
+util.global private @device_a : !hal.device
+util.global private @device_b : !hal.device
+
+// CHECK-LABEL: @scfControlFlowWithAffinities
+util.func public @scfControlFlowWithAffinities(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c2 = arith.constant 2 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c0_i32 = arith.constant 0 : i32
+
+ // Partition on device_a.
+ // CHECK-NOT: stream.async.execute
+ %clone_a = stream.async.clone on(#hal.device.affinity<@device_a>) %arg0 : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+
+ // Control flow on host.
+ // CHECK-NOT: stream.timepoint.await
+ // CHECK: scf.for
+ %result = scf.for %i = %c0 to %c2 step %c1 iter_args(%iter = %c0_i32) -> (i32) {
+ // Partition on device_a for clone inside loop.
+ // CHECK: stream.async.execute on(#hal.device.affinity<@device_a>)
+ // CHECK-NEXT: stream.async.clone
+ // Partition on device_b inside loop.
+ // CHECK: stream.async.execute on(#hal.device.affinity<@device_b>)
+ // CHECK-NEXT: stream.async.slice
+ // CHECK-NEXT: stream.async.transfer
+ %slice = stream.async.slice on(#hal.device.affinity<@device_b>) %clone_a[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ %transfer = stream.async.transfer %slice : !stream.resource<external>{%c4} from(#hal.device.affinity<@device_b>) -> to(#hal.device.affinity<@device_b>) !stream.resource<staging>{%c4}
+ // CHECK: stream.timepoint.await
+ %load = stream.async.load %transfer[%c0] : !stream.resource<staging>{%c4} -> i32
+ %add = arith.addi %iter, %load : i32
+ scf.yield %add : i32
+ }
+
+ // Final partition on device_a.
+ // CHECK: stream.async.execute on(#hal.device.affinity<@device_a>)
+ // CHECK-NEXT: stream.async.splat
+ %splat = stream.async.splat on(#hal.device.affinity<@device_a>) %result : i32 -> !stream.resource<external>{%c4}
+ // CHECK: stream.timepoint.await
+ util.return %splat : !stream.resource<external>
+}
+
+// -----
+
+// Tests that operations captured in nested regions (scf.for) are not grouped
+// with operations that depend on the nested region's results. This prevents
+// circular dependencies where the partition would use the scf result while the
+// scf captures values from the partition.
+
+// CHECK-LABEL: @scfNestedCapture
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<*>, %[[ARG1:.+]]: index)
+util.func public @scfNestedCapture(%arg0: !stream.resource<*>, %arg1: index) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c10 = arith.constant 10 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ // The cloned resource will be duplicated - one copy in the loop's partition,
+ // one copy in the dispatch's partition. No standalone clone partition.
+ %cloned = stream.async.clone %arg0 : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+
+ // Loop gets its own materialized clone.
+ // CHECK: %[[LOOP_RESULT:.+]] = scf.for
+ %loop_result = scf.for %i = %c0 to %c10 step %c1 iter_args(%acc = %c0_i64) -> (i64) {
+ // Operations inside loop should be partitioned with cloned op materialized.
+ // CHECK: stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]]
+ // CHECK-NEXT: stream.async.clone
+ // CHECK: stream.async.transfer
+ %slice = stream.async.slice %cloned[%c0 to %arg1] : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+ // CHECK: stream.timepoint.await
+ // CHECK: stream.async.load
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%arg1} -> i32
+ %loaded_i64 = arith.extsi %loaded : i32 to i64
+ %new_acc = arith.addi %acc, %loaded_i64 : i64
+ scf.yield %new_acc : i64
+ }
+
+ // Dispatch doesn't use %cloned, only %loop_result, so no clone is materialized here.
+ // Before the fix, partitioning would try to group %cloned with this dispatch,
+ // creating a circular dependency (dispatch needs loop result, loop needs cloned).
+ // After fix, %cloned is only materialized where used (in loop partition).
+ // CHECK: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ // CHECK: stream.async.dispatch
+ %dispatched = stream.async.dispatch @ex::@dispatch[%c1](%loop_result) : (i64) -> !stream.resource<*>{%arg1}
+
+ util.return %dispatched : !stream.resource<*>
+}
+
+// -----
+
+// Tests scf.if with divergent captures - only one branch captures from parent.
+// Partitioning must be conservative and treat the containing scf.if as a hazard.
+
+// CHECK-LABEL: @scfIfDivergentCapture
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<*>, %[[ARG1:.+]]: index, %[[COND:.+]]: i1)
+util.func public @scfIfDivergentCapture(%arg0: !stream.resource<*>, %arg1: index, %cond: i1) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ %cloned = stream.async.clone %arg0 : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+
+ // Only true branch captures %cloned - it will get its own materialized clone.
+ // CHECK: %[[IF_RESULT:.+]] = scf.if
+ %if_result = scf.if %cond -> (i64) {
+ // CHECK: stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]]
+ // CHECK-NEXT: stream.async.clone
+ %slice = stream.async.slice %cloned[%c0 to %arg1] : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%arg1} -> i32
+ %result = arith.extsi %loaded : i32 to i64
+ scf.yield %result : i64
+ } else {
+ // False branch doesn't capture anything.
+ scf.yield %c0_i64 : i64
+ }
+
+ // Dispatch doesn't use %cloned, so no clone is materialized here.
+ // CHECK: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ // CHECK: stream.async.dispatch
+ %dispatched = stream.async.dispatch @ex::@dispatch[%c1](%if_result) : (i64) -> !stream.resource<*>{%arg1}
+
+ util.return %dispatched : !stream.resource<*>
+}
+
+// -----
+
+// Tests nested scf.for loops where inner loop captures from grandparent scope.
+// The containing operation walk should find the outer scf.for correctly.
+
+// CHECK-LABEL: @scfNestedLoopCapture
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<*>, %[[ARG1:.+]]: index)
+util.func public @scfNestedLoopCapture(%arg0: !stream.resource<*>, %arg1: index) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c5 = arith.constant 5 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ %cloned = stream.async.clone %arg0 : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+
+ // Outer loop.
+ // Inner loop gets its own materialized clone.
+ // CHECK: %[[OUTER_RESULT:.+]] = scf.for
+ %outer_result = scf.for %i = %c0 to %c5 step %c1 iter_args(%outer_acc = %c0_i64) -> (i64) {
+ // Inner loop captures %cloned from grandparent block.
+ // It gets its own materialized clone.
+ // CHECK: scf.for
+ %inner_result = scf.for %j = %c0 to %c5 step %c1 iter_args(%inner_acc = %c0_i64) -> (i64) {
+ // CHECK: stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]]
+ // CHECK-NEXT: stream.async.clone
+ %slice = stream.async.slice %cloned[%c0 to %arg1] : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%arg1} -> i32
+ %loaded_i64 = arith.extsi %loaded : i32 to i64
+ %new_inner = arith.addi %inner_acc, %loaded_i64 : i64
+ scf.yield %new_inner : i64
+ }
+ %new_outer = arith.addi %outer_acc, %inner_result : i64
+ scf.yield %new_outer : i64
+ }
+
+ // Dispatch doesn't use %cloned, so no clone is materialized here.
+ // CHECK: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ // CHECK: stream.async.dispatch
+ %dispatched = stream.async.dispatch @ex::@dispatch[%c1](%outer_result) : (i64) -> !stream.resource<*>{%arg1}
+
+ util.return %dispatched : !stream.resource<*>
+}
+
+// -----
+
+// Tests multiple scf operations at the same level capturing the same resource.
+// All should mark the resource as hazardous for grouping with their consumers.
+
+// CHECK-LABEL: @scfMultipleCaptures
+// CHECK-SAME: (%[[ARG0:.+]]: !stream.resource<*>, %[[ARG1:.+]]: index)
+util.func public @scfMultipleCaptures(%arg0: !stream.resource<*>, %arg1: index) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c5 = arith.constant 5 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ %cloned = stream.async.clone %arg0 : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+
+ // First loop gets its own materialized clone.
+ // CHECK: %[[RESULT1:.+]] = scf.for
+ %result1 = scf.for %i = %c0 to %c5 step %c1 iter_args(%acc = %c0_i64) -> (i64) {
+ // CHECK: stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]]
+ // CHECK-NEXT: stream.async.clone
+ %slice = stream.async.slice %cloned[%c0 to %arg1] : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%arg1} -> i32
+ %loaded_i64 = arith.extsi %loaded : i32 to i64
+ %new_acc = arith.addi %acc, %loaded_i64 : i64
+ scf.yield %new_acc : i64
+ }
+
+ // Second loop also gets its own materialized clone.
+ // CHECK: %[[RESULT2:.+]] = scf.for
+ %result2 = scf.for %i = %c0 to %c5 step %c1 iter_args(%acc = %c0_i64) -> (i64) {
+ // CHECK: stream.async.execute
+ // CHECK-SAME: with(%[[ARG0]]
+ // CHECK-NEXT: stream.async.clone
+ %slice = stream.async.slice %cloned[%c0 to %arg1] : !stream.resource<*>{%arg1} -> !stream.resource<*>{%arg1}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%arg1} -> !stream.resource<staging>{%arg1}
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%arg1} -> i32
+ %loaded_i64 = arith.extsi %loaded : i32 to i64
+ %new_acc = arith.addi %acc, %loaded_i64 : i64
+ scf.yield %new_acc : i64
+ }
+
+ // Dispatch uses both loop results. Should be in separate partition from %cloned.
+ // CHECK: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ // CHECK: stream.async.dispatch
+ %sum = arith.addi %result1, %result2 : i64
+ %dispatched = stream.async.dispatch @ex::@dispatch[%c1](%sum) : (i64) -> !stream.resource<*>{%arg1}
+
+ util.return %dispatched : !stream.resource<*>
+}
+
+// -----
+
+// Tests that nested scf operations correctly handle iter_args pattern.
+// Iter_args should allow values to flow through the loop without creating hazards.
+
+// CHECK-LABEL: @scfIterArgsPattern
+util.func public @scfIterArgsPattern(%arg0: !stream.resource<*>, %arg1: index) -> !stream.resource<*> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c5 = arith.constant 5 : index
+
+ // Loop with iter_args - resource flows through the loop correctly.
+ // CHECK: scf.for
+ // CHECK-SAME: iter_args
+ %loop_result = scf.for %i = %c0 to %c5 step %c1 iter_args(%iter_resource = %arg0) -> !stream.resource<*> {
+ // Each iteration partitions independently.
+ // CHECK: stream.async.execute
+ // CHECK: stream.async.dispatch
+ %updated = stream.async.dispatch @ex::@dispatch[%c1](%iter_resource[%c0 to %arg1 for %arg1]) : (!stream.resource<*>{%arg1}) -> !stream.resource<*>{%arg1}
+ // CHECK: stream.timepoint.await
+ scf.yield %updated : !stream.resource<*>
+ }
+
+ util.return %loop_result : !stream.resource<*>
+}
+
+// -----
+
+// Tests mixed iter_args (good) and captures (hazard) in same loop.
+// This is a common real-world pattern where a loop updates one resource via iter_args
+// while reading from another resource captured from parent scope.
+
+// CHECK-LABEL: @scfMixedIterArgsAndCaptures
+util.func public @scfMixedIterArgsAndCaptures(%arg0: !stream.resource<*>, %arg1: !stream.resource<*>, %size: index) -> (!stream.resource<*>, i64) {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c5 = arith.constant 5 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ // This resource will be materialized in the loop.
+ %read_only = stream.async.clone %arg1 : !stream.resource<*>{%size} -> !stream.resource<*>{%size}
+
+ // Loop: %arg0 flows through iter_args (updated each iteration),
+ // %read_only materialized in loop (read each iteration),
+ // %accumulator tracks scalar across iterations.
+ // CHECK: %[[LOOP_RESULTS:.+]]:2 = scf.for
+ // CHECK-SAME: iter_args(
+ %loop_resource, %loop_sum = scf.for %i = %c0 to %c5 step %c1
+ iter_args(%iter_resource = %arg0, %iter_sum = %c0_i64) -> (!stream.resource<*>, i64) {
+
+ // Inside loop: both iter_arg resource and materialized clone are used.
+ // They must be in the same execute region since the dispatch needs the
+ // updated iter_arg, and the transfer needs the materialized clone.
+ // CHECK: stream.async.execute
+ // CHECK: stream.async.clone
+ // CHECK: stream.async.dispatch
+ // CHECK: stream.async.transfer
+ %updated = stream.async.dispatch @ex::@dispatch[%c1](%iter_resource[%c0 to %size for %size])
+ : (!stream.resource<*>{%size}) -> !stream.resource<*>{%size}
+
+ %slice = stream.async.slice %read_only[%c0 to %size] : !stream.resource<*>{%size} -> !stream.resource<*>{%size}
+ %transfer = stream.async.transfer %slice : !stream.resource<*>{%size} -> !stream.resource<staging>{%size}
+ %loaded = stream.async.load %transfer[%c0] : !stream.resource<staging>{%size} -> i32
+ %loaded_i64 = arith.extsi %loaded : i32 to i64
+ %new_sum = arith.addi %iter_sum, %loaded_i64 : i64
+
+ scf.yield %updated, %new_sum : !stream.resource<*>, i64
+ }
+
+ // The key test: the final dispatch uses loop results but NOT the captured
+ // %read_only. This proves that %read_only is correctly tracked as a hazard
+ // and not incorrectly merged with operations that only use the loop's outputs.
+ // CHECK: stream.async.execute
+ // CHECK-NOT: stream.async.clone
+ // CHECK: stream.async.dispatch
+ %final = stream.async.dispatch @ex::@dispatch[%c1](%loop_sum, %loop_resource[%c0 to %size for %size])
+ : (i64, !stream.resource<*>{%size}) -> !stream.resource<*>{%size}
+
+ util.return %final, %loop_sum : !stream.resource<*>, i64
+}
+
+// -----
+
+// Tests splat with optimization_barrier in same block + use inside scf.if.
+// This reproduces the bug from if.mlir: splat with barrier use AND use in nested
+// region was not being placed in any partition, causing verification failure.
+// The barrier forces the splat into a partition before the scf.if.
+
+// CHECK-LABEL: @splatWithBarrierAndScfIfUse
+util.func public @splatWithBarrierAndScfIfUse(%cond: i1, %arg0: !stream.resource<external>) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c10_i32 = arith.constant 10 : i32
+ %c4 = arith.constant 4 : index
+
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with() -> !stream.resource<external>{%c4}
+ // CHECK-NEXT: %[[SPLAT:.+]] = stream.async.splat %c10_i32
+ // CHECK-NEXT: stream.yield %[[SPLAT]]
+ %splat = stream.async.splat %c10_i32 : i32 -> !stream.resource<external>{%c4}
+
+ // Same-block non-streamable user forces await before scf.if.
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[TIMEPOINT]]
+ %barrier = util.optimization_barrier %splat : !stream.resource<external>
+
+ // CHECK: scf.if
+ %result = scf.if %cond -> !stream.resource<external> {
+ // Nested region can use both the awaited value and reference original.
+ // CHECK: stream.async.execute
+ // CHECK: stream.async.dispatch
+ %dispatch = stream.async.dispatch @dispatch::@entry(%barrier[%c0 to %c4 for %c4], %splat[%c0 to %c4 for %c4]) : (!stream.resource<external>{%c4}, !stream.resource<external>{%c4}) -> !stream.resource<external>{%c4}
+ scf.yield %dispatch : !stream.resource<external>
+ } else {
+ scf.yield %barrier : !stream.resource<external>
+ }
+
+ util.return %result : !stream.resource<external>
+}
+
+stream.executable private @dispatch {
+ stream.executable.export public @entry
+ builtin.module {
+ func.func @entry(%arg0: !stream.binding, %arg1: !stream.binding, %arg2: !stream.binding) {
+ return
+ }
+ }
+}
+
+// -----
+
+// Tests clone with optimization_barrier in same block + use inside scf.for.
+// Similar to test above but with clone + scf.for instead of splat + scf.if.
+
+// CHECK-LABEL: @cloneWithBarrierAndScfForUse
+util.func public @cloneWithBarrierAndScfForUse(%arg0: !stream.resource<external>, %ub: index) -> i64 {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+ %c0_i64 = arith.constant 0 : i64
+
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[CLONE:.+]] = stream.async.clone %[[ARG]]
+ // CHECK-NEXT: stream.yield %[[CLONE]]
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[TIMEPOINT]]
+ %barrier = util.optimization_barrier %clone : !stream.resource<external>
+
+ // CHECK: scf.for
+ %result = scf.for %i = %c0 to %ub step %c1 iter_args(%iter = %c0_i64) -> (i64) {
+ // CHECK: stream.async.execute
+ // CHECK: stream.async.slice
+ // CHECK: stream.async.transfer
+ %results_0, %result_timepoint_1 = stream.async.execute with(%barrier as %arg1: !stream.resource<external>{%c8}) -> !stream.resource<staging>{%c4} {
+ %3 = stream.async.slice %arg1[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ %4 = stream.async.transfer %3 : !stream.resource<external>{%c4} -> !stream.resource<staging>{%c4}
+ stream.yield %4 : !stream.resource<staging>{%c4}
+ } => !stream.timepoint
+ %1 = stream.timepoint.await %result_timepoint_1 => %results_0 : !stream.resource<staging>{%c4}
+ %2 = stream.async.load %1[%c0] : !stream.resource<staging>{%c4} -> i32
+ %ext = arith.extsi %2 : i32 to i64
+ %next = arith.addi %iter, %ext : i64
+ scf.yield %next : i64
+ }
+
+ util.return %result : i64
+}
+
+// -----
+
+// Tests splat with multiple optimization_barrier uses in same block.
+// Ensures that multiple same-block non-streamable users work correctly.
+
+// CHECK-LABEL: @splatWithMultipleBarriers
+util.func public @splatWithMultipleBarriers() -> (!stream.resource<external>, !stream.resource<external>) {
+ %c10_i32 = arith.constant 10 : i32
+ %c4 = arith.constant 4 : index
+
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-NEXT: %[[SPLAT:.+]] = stream.async.splat
+ // CHECK-NEXT: stream.yield %[[SPLAT]]
+ %splat = stream.async.splat %c10_i32 : i32 -> !stream.resource<external>{%c4}
+
+ // Both barriers use the same awaited value.
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[TIMEPOINT]]
+ // CHECK: util.optimization_barrier %[[AWAITED]]
+ %barrier1 = util.optimization_barrier %splat : !stream.resource<external>
+ // CHECK: util.optimization_barrier %[[AWAITED]]
+ %barrier2 = util.optimization_barrier %splat : !stream.resource<external>
+
+ util.return %barrier1, %barrier2 : !stream.resource<external>, !stream.resource<external>
+}
+
+// -----
+
+// Tests clone with optimization_barrier + use in deeply nested regions.
+// Ensures barrier causes partitioning even with deep nesting (scf.for > scf.if).
+
+// CHECK-LABEL: @clonableBarrierDeeplyNested
+util.func public @clonableBarrierDeeplyNested(%arg0: !stream.resource<external>, %cond: i1, %ub: index) -> !stream.resource<external> {
+ %c0 = arith.constant 0 : index
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c8 = arith.constant 8 : index
+
+ // CHECK: %[[RESULTS:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ // CHECK-SAME: with(%{{.+}} as %[[ARG:.+]]: !stream.resource<external>{%c8})
+ // CHECK-SAME: -> !stream.resource<external>{%c8}
+ // CHECK-NEXT: %[[CLONE:.+]] = stream.async.clone %[[ARG]]
+ // CHECK-NEXT: stream.yield %[[CLONE]]
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c8} -> !stream.resource<external>{%c8}
+
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[TIMEPOINT]]
+ %barrier = util.optimization_barrier %clone : !stream.resource<external>
+
+ // CHECK: scf.for
+ %result = scf.for %i = %c0 to %ub step %c1 iter_args(%iter = %arg0) -> !stream.resource<external> {
+ // CHECK: scf.if
+ %next = scf.if %cond -> !stream.resource<external> {
+ // Deeply nested use of barrier.
+ // CHECK: stream.async.execute
+ // CHECK: stream.async.slice
+ %results_0, %result_timepoint_1 = stream.async.execute with(%barrier as %arg1: !stream.resource<external>{%c8}) -> !stream.resource<external>{%c4} {
+ %2 = stream.async.slice %arg1[%c0 to %c4] : !stream.resource<external>{%c8} -> !stream.resource<external>{%c4}
+ stream.yield %2 : !stream.resource<external>{%c4}
+ } => !stream.timepoint
+ %1 = stream.timepoint.await %result_timepoint_1 => %results_0 : !stream.resource<external>{%c4}
+ scf.yield %1 : !stream.resource<external>
+ } else {
+ scf.yield %iter : !stream.resource<external>
+ }
+ scf.yield %next : !stream.resource<external>
+ }
+
+ util.return %result : !stream.resource<external>
+}
diff --git a/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_timeline_aware.mlir b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_timeline_aware.mlir
new file mode 100644
index 0000000..1933bde
--- /dev/null
+++ b/compiler/src/iree/compiler/Dialect/Stream/Transforms/test/schedule_execution_timeline_aware.mlir
@@ -0,0 +1,118 @@
+// RUN: iree-opt --split-input-file --pass-pipeline="builtin.module(util.func(iree-stream-schedule-execution))" %s | FileCheck %s
+
+// Tests that timeline-aware ops don't get immediate awaits.
+// The TimelineAwareOpInterface allows ops to participate in timeline scheduling.
+
+// CHECK-LABEL: @timelineAwareCall
+util.func public @timelineAwareCall(%resource: !stream.resource<external>, %signal_fence: !stream.test.fence) -> !stream.resource<external> {
+ %c16384 = arith.constant 16384 : index
+
+ // Create async execution that produces a timepoint.
+ // CHECK: %[[RESULT:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ %result, %timepoint = stream.async.execute with(%resource as %arg0: !stream.resource<external>{%c16384}) -> !stream.resource<external>{%c16384} {
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16384} -> !stream.resource<external>{%c16384}
+ stream.yield %clone : !stream.resource<external>{%c16384}
+ } => !stream.timepoint
+
+ // Export timepoint to fence for timeline-aware op.
+ // CHECK-NOT: stream.timepoint.await %[[TIMEPOINT]]
+ // CHECK: %[[WAIT_FENCE:.+]] = stream.timepoint.export %[[TIMEPOINT]]
+ %wait_fence = stream.timepoint.export %timepoint => (!stream.test.fence)
+
+ // Call timeline-aware op - should NOT have await before it.
+ // CHECK: stream.test.timeline_aware(%[[RESULT]]) waits(%[[WAIT_FENCE]]) signals(%{{.+}})
+ %computed = stream.test.timeline_aware(%result) waits(%wait_fence) signals(%signal_fence) : (!stream.resource<external>) -> !stream.resource<external>
+
+ // Import signal fence back to timepoint.
+ // CHECK: %[[IMPORTED_TP:.+]] = stream.timepoint.import
+ %imported_tp = stream.timepoint.import %signal_fence : (!stream.test.fence) => !stream.timepoint
+
+ // Subsequent async.execute should await imported timepoint.
+ // CHECK: %[[FINAL:.+]], %{{.+}} = stream.async.execute await(%[[IMPORTED_TP]])
+ %awaited = stream.timepoint.await %imported_tp => %computed : !stream.resource<external>{%c16384}
+ %final, %final_tp = stream.async.execute with(%awaited as %arg0: !stream.resource<external>{%c16384}) -> !stream.resource<external>{%c16384} {
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16384} -> !stream.resource<external>{%c16384}
+ stream.yield %clone : !stream.resource<external>{%c16384}
+ } => !stream.timepoint
+
+ // CHECK: util.return %[[FINAL]]
+ util.return %final : !stream.resource<external>
+}
+
+// -----
+
+// Tests timeline awareness between multiple sequential timeline-aware calls.
+
+// CHECK-LABEL: @sequentialTimelineAwareCalls
+util.func public @sequentialTimelineAwareCalls(%resource: !stream.resource<external>, %fence_a: !stream.test.fence, %fence_b: !stream.test.fence) -> !stream.resource<external> {
+ %c16384 = arith.constant 16384 : index
+
+ // First async region.
+ // CHECK: %[[R1:.+]], %[[T1:.+]] = stream.async.execute
+ %r1, %t1 = stream.async.execute with(%resource as %arg0: !stream.resource<external>{%c16384}) -> !stream.resource<external>{%c16384} {
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16384} -> !stream.resource<external>{%c16384}
+ stream.yield %clone : !stream.resource<external>{%c16384}
+ } => !stream.timepoint
+
+ // First timeline-aware call - should NOT have await before it.
+ // CHECK-NOT: stream.timepoint.await %[[T1]]
+ // CHECK: %[[WAIT_A:.+]] = stream.timepoint.export %[[T1]]
+ %wait_a = stream.timepoint.export %t1 => (!stream.test.fence)
+ // CHECK: %[[RESULT_A:.+]] = stream.test.timeline_aware(%[[R1]]) waits(%[[WAIT_A]]) signals(%{{.+}})
+ %result_a = stream.test.timeline_aware(%r1) waits(%wait_a) signals(%fence_a) : (!stream.resource<external>) -> !stream.resource<external>
+
+ // Import result from first call.
+ // CHECK: %[[TP_A:.+]] = stream.timepoint.import
+ %tp_a = stream.timepoint.import %fence_a : (!stream.test.fence) => !stream.timepoint
+
+ // Second async region - pass optimizes await into async.execute.
+ // CHECK: %[[R2:.+]], %[[T2:.+]] = stream.async.execute await(%[[TP_A]]) => with(%[[RESULT_A]] as %{{.+}}: !stream.resource<external>{%c16384})
+ %awaited_a = stream.timepoint.await %tp_a => %result_a : !stream.resource<external>{%c16384}
+ %r2, %t2 = stream.async.execute with(%awaited_a as %arg0: !stream.resource<external>{%c16384}) -> !stream.resource<external>{%c16384} {
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16384} -> !stream.resource<external>{%c16384}
+ stream.yield %clone : !stream.resource<external>{%c16384}
+ } => !stream.timepoint
+
+ // Second timeline-aware call - again, no await before.
+ // CHECK-NOT: stream.timepoint.await %[[T2]]
+ // CHECK: %[[WAIT_B:.+]] = stream.timepoint.export %[[T2]]
+ %wait_b = stream.timepoint.export %t2 => (!stream.test.fence)
+ // CHECK: %[[RESULT_B:.+]] = stream.test.timeline_aware(%[[R2]]) waits(%[[WAIT_B]]) signals(%{{.+}})
+ %result_b = stream.test.timeline_aware(%r2) waits(%wait_b) signals(%fence_b) : (!stream.resource<external>) -> !stream.resource<external>
+
+ // Import and wait for second call.
+ // CHECK: %[[TP_B:.+]] = stream.timepoint.import
+ %tp_b = stream.timepoint.import %fence_b : (!stream.test.fence) => !stream.timepoint
+ // CHECK: %[[AWAITED_B:.+]] = stream.timepoint.await %[[TP_B]] => %[[RESULT_B]]
+ %awaited_b = stream.timepoint.await %tp_b => %result_b : !stream.resource<external>{%c16384}
+
+ // CHECK: util.return %[[AWAITED_B]]
+ util.return %awaited_b : !stream.resource<external>
+}
+
+// -----
+
+// Tests that non-timeline-aware ops still get normal awaits.
+
+util.func private @normal_func(!stream.resource<external>) -> !stream.resource<external>
+
+// CHECK-LABEL: @nonTimelineAwareCall
+util.func public @nonTimelineAwareCall(%resource: !stream.resource<external>) -> !stream.resource<external> {
+ %c16384 = arith.constant 16384 : index
+
+ // Async work.
+ // CHECK: %[[RESULT:.+]], %[[TIMEPOINT:.+]] = stream.async.execute
+ %result, %timepoint = stream.async.execute with(%resource as %arg0: !stream.resource<external>{%c16384}) -> !stream.resource<external>{%c16384} {
+ %clone = stream.async.clone %arg0 : !stream.resource<external>{%c16384} -> !stream.resource<external>{%c16384}
+ stream.yield %clone : !stream.resource<external>{%c16384}
+ } => !stream.timepoint
+
+ // Normal call without timeline awareness - SHOULD have await.
+ // CHECK: %[[AWAITED:.+]] = stream.timepoint.await %[[TIMEPOINT]] => %[[RESULT]]
+ %awaited = stream.timepoint.await %timepoint => %result : !stream.resource<external>{%c16384}
+
+ // CHECK: util.call @normal_func(%[[AWAITED]])
+ %called = util.call @normal_func(%awaited) : (!stream.resource<external>) -> !stream.resource<external>
+
+ util.return %called : !stream.resource<external>
+}