Skip to content

Version aware fusion plan#945

Open
Melikano wants to merge 26 commits into
hw-native-sys:mainfrom
afshinarefi:version-aware-fusion-plan
Open

Version aware fusion plan#945
Melikano wants to merge 26 commits into
hw-native-sys:mainfrom
afshinarefi:version-aware-fusion-plan

Conversation

@Melikano

Copy link
Copy Markdown
Contributor

Summary

  • Added version-aware tile-fusion planning based on each TileOp’s legal implementation candidates.
  • Preserved the existing block-local greedy assignment while allowing multiple versioned group states to be evaluated per seed.
  • Deferred final implementation selection to downstream passes.

Implementation

  • Added shared candidate-metadata parsing, construction, and validation utilities.
  • Added version-aware group enumeration with deterministic scoring and tie-breaking.
  • Calculated costs from the complete fusion group using base DAG and temporary version-trait terms.
  • Narrowed each selected operation’s candidates list to all equally best-scoring versions without reordering or selecting a final
    version.
  • Preserved the downstream pto.fusion.group_id and pto.fusion.order contract.
  • Added the version-aware planning design document.

Tests

  • fusion_plan_alternate_versioned_path.pto

    • Verifies exploration of alternate data-flow paths and selection of the better version-aware group.
  • fusion_plan_version_score_skips_bad_extension.pto

    • Verifies that version penalties can make a smaller non-terminal group preferable to a larger extension.
  • fusion_plan_retains_tied_candidates.pto

    • Verifies that equally scored candidates are retained while lower-scoring candidates are removed.

Melikano and others added 23 commits July 15, 2026 12:36

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces version-aware planning to the Tile Fusion planner, allowing it to evaluate and compare fusion groups based on both compute nodes and their selected implementation versions. It refactors template candidate metadata parsing into a shared utility and updates the fusion planner to enumerate versioned states, score them with an extended cost model, and narrow candidate lists accordingly. The review feedback highlights two key improvements: caching the parsed template candidate versions in enumerateVersionedStatesFromSeed to avoid redundant parsing in a nested loop, and replacing a non-standard getAs<IntegerAttr> call on DictionaryAttr with dyn_cast_or_null to ensure MLIR version compatibility.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +635 to +688
static FailureOr<SmallVector<GroupState, 16>>
enumerateVersionedStatesFromSeed(const PlanningContext &ctx,
const CostModel &costModel,
const pto::FusionComputeNode &seed,
const DenseSet<unsigned> &assignedNodes) {
FailureOr<SmallVector<GroupState, 4>> seedStates = createSeedStates(seed);
if (failed(seedStates))
return failure();

SmallVector<GroupState, 16> validStates;
SmallVector<GroupState, 16> frontier(seedStates->begin(), seedStates->end());
std::set<std::string> seenStates;
for (const GroupState &state : frontier)
seenStates.insert(getStateSignature(state));

// Explore all reachable dataflow-connected groups for this seed, not just
// the first greedy path through block order. The state signature includes the
// selected implementation version for each node, so the same node set can be
// scored multiple ways when TileOp template candidates differ.
while (!frontier.empty()) {
GroupState state = std::move(frontier.pop_back_val());
for (const pto::FusionComputeNode &candidate :
ctx.blockAnalysis.computeNodes) {
if (assignedNodes.contains(candidate.id) || state.contains(candidate))
continue;

FailureOr<SmallVector<TileOpImplVersion, 4>> versions =
getLegalImplVersions(candidate);
if (failed(versions))
return failure();

for (const TileOpImplVersion &version : *versions) {
FailureOr<std::optional<GroupState>> maybeNextState =
tryAppendVersionedCandidate(ctx, costModel, state, candidate,
version);
if (failed(maybeNextState))
return failure();
if (!*maybeNextState)
continue;

GroupState nextState = std::move(**maybeNextState);
std::string signature = getStateSignature(nextState);
if (!seenStates.insert(signature).second)
continue;

if (nextState.members.size() >= 2)
validStates.push_back(nextState);
frontier.push_back(std::move(nextState));
}
}
}

return validStates;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Calling getLegalImplVersions inside the nested loop of enumerateVersionedStatesFromSeed is highly inefficient. It parses the template candidate attributes from the operation every time, which involves dictionary lookups, attribute casting, and dynamic memory allocations for std::string names. Since the candidate nodes in the block do not change during the enumeration, these versions should be pre-parsed and cached at the start of the function to avoid redundant parsing.

static FailureOr<SmallVector<GroupState, 16>>
enumerateVersionedStatesFromSeed(const PlanningContext &ctx,
                                 const CostModel &costModel,
                                 const pto::FusionComputeNode &seed,
                                 const DenseSet<unsigned> &assignedNodes) {
  DenseMap<unsigned, SmallVector<TileOpImplVersion, 4>> cachedVersions;
  for (const pto::FusionComputeNode &node : ctx.blockAnalysis.computeNodes) {
    FailureOr<SmallVector<TileOpImplVersion, 4>> versions = getLegalImplVersions(node);
    if (failed(versions))
      return failure();
    cachedVersions[node.id] = std::move(*versions);
  }

  auto seedIt = cachedVersions.find(seed.id);
  if (seedIt == cachedVersions.end())
    return failure();

  SmallVector<GroupState, 16> validStates;
  SmallVector<GroupState, 16> frontier;
  for (const TileOpImplVersion &version : seedIt->second) {
    GroupState state;
    state.append(PlannedFusionMember{&seed, version});
    frontier.push_back(std::move(state));
  }

  std::set<std::string> seenStates;
  for (const GroupState &state : frontier)
    seenStates.insert(getStateSignature(state));

  // Explore all reachable dataflow-connected groups for this seed, not just
  // the first greedy path through block order. The state signature includes the
  // selected implementation version for each node, so the same node set can be
  // scored multiple ways when TileOp template candidates differ.
  while (!frontier.empty()) {
    GroupState state = std::move(frontier.pop_back_val());
    for (const pto::FusionComputeNode &candidate :
         ctx.blockAnalysis.computeNodes) {
      if (assignedNodes.contains(candidate.id) || state.contains(candidate))
        continue;

      auto candIt = cachedVersions.find(candidate.id);
      if (candIt == cachedVersions.end())
        return failure();
      const SmallVector<TileOpImplVersion, 4> &versions = candIt->second;

      for (const TileOpImplVersion &version : versions) {
        FailureOr<std::optional<GroupState>> maybeNextState =
            tryAppendVersionedCandidate(ctx, costModel, state, candidate,
                                        version);
        if (failed(maybeNextState))
          return failure();
        if (!*maybeNextState)
          continue;

        GroupState nextState = std::move(**maybeNextState);
        std::string signature = getStateSignature(nextState);
        if (!seenStates.insert(signature).second)
          continue;

        if (nextState.members.size() >= 2)
          validStates.push_back(nextState);
        frontier.push_back(std::move(nextState));
      }
    }
  }

  return validStates;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed by adding cache to prevent redundant metadata parsing

Comment on lines +314 to +315
auto id = candidate.getAs<IntegerAttr>(pto::kTemplateCandidateIdAttr);
if (!id)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using getAs<IntegerAttr> on DictionaryAttr is not standard in MLIR and might lead to compilation errors depending on the exact MLIR version. It is safer and more consistent with the rest of the codebase (e.g., TemplateAttributes.cpp) to use dyn_cast_or_null<IntegerAttr>(candidate.get(...)).

Suggested change
auto id = candidate.getAs<IntegerAttr>(pto::kTemplateCandidateIdAttr);
if (!id)
auto id = dyn_cast_or_null<IntegerAttr>(candidate.get(pto::kTemplateCandidateIdAttr));
if (!id)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant