Skip to content

improve optimization complete logic#4828

Closed
mgarrard wants to merge 1 commit intofacebook:mainfrom
mgarrard:export-D91549954
Closed

improve optimization complete logic#4828
mgarrard wants to merge 1 commit intofacebook:mainfrom
mgarrard:export-D91549954

Conversation

@mgarrard
Copy link
Copy Markdown
Contributor

Summary:
This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954

@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented Jan 27, 2026

@mgarrard has exported this pull request. If you are a Meta employee, you can view the originating Diff in D91549954.

@meta-cla meta-cla Bot added the CLA Signed Do not delete this pull request or issue due to inactivity. label Jan 27, 2026
@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Jan 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.75%. Comparing base (ce4dc42) to head (9e28603).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4828   +/-   ##
=======================================
  Coverage   96.75%   96.75%           
=======================================
  Files         591      591           
  Lines       61874    61882    +8     
=======================================
+ Hits        59869    59877    +8     
  Misses       2005     2005           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 27, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 27, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 27, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 27, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 27, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Jan 28, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 4, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 5, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 5, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 5, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 9, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Reviewed By: lena-kashtelyan

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 9, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Reviewed By: lena-kashtelyan

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 9, 2026
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Reviewed By: lena-kashtelyan

Differential Revision: D91549954
mgarrard added a commit to mgarrard/Ax that referenced this pull request Feb 9, 2026
Summary:
Pull Request resolved: facebook#4828

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue

        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )

        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete

    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Reviewed By: lena-kashtelyan

Differential Revision: D91549954
Summary:

This criteria updates the completion state logic to assume if a node can transition, and that transition is to itself, then the optimization is complete.

This works because should_transition_to_next_node only considers transtion blocking criteria (ie not max parallelism) when thinking about should transition or not. And if a node points to itself, we can assume that signifies the end of the optimiztion (steps are initialized this way earlier in this stack). this allows allows for the gs to be re-called into, and the tc criterion to change thus putting it back into a non-complete state.

An alternative I considered is to check if all transition edges are completed, and at least one points to self. This would look something like the below snippet. It would be much more expensive to evaluate, and is guarding against a malformed strategy. Edges are already known to be created in order of importance, and self transition edges should be considered ending edges when their importance is considered

```
property
def optimization_complete(self) -> bool:
    if len(self._curr.transition_criteria) == 0:
        return False

    # Check ALL transition edges, not just the first matching one
    for next_node, all_tc in self._curr.transition_edges.items():
        transition_blocking = [tc for tc in all_tc if tc.block_transition_if_unmet]
        if not transition_blocking:
            continue
        
        all_met = all(
            tc.is_met(experiment=self.experiment, curr_node=self._curr)
            for tc in transition_blocking
        )
        
        if all_met:
            # An edge's criteria are met - check where it points
            if next_node != self._curr.name:
                return False  # Can transition to different node, not complete
    
    # All met edges (if any) point to self
    # Check if we actually have any met criteria pointing to self
    can_transition, next_node = self._curr.should_transition_to_next_node(
        raise_data_required_error=False
    )
    return can_transition and next_node == self._curr.name
```

The thrid alternative is to instate "compeletion node", which i think could be viable in the future if we have more complex generation strategies than we currently support, and the self generation logic is too cumbersome.

For now though, I think this is a pretty nice simplification that also should have some compute wins. Going from O (number of nodes * number of TC per node), to O(number of tc on current node)

Reviewed By: lena-kashtelyan

Differential Revision: D91549954
@meta-codesync
Copy link
Copy Markdown

meta-codesync Bot commented Feb 10, 2026

This pull request has been merged in ed975ea.

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

Labels

CLA Signed Do not delete this pull request or issue due to inactivity. fb-exported Merged meta-exported

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants