Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add NaryReduceNode and InputNode #166

Open
wants to merge 20 commits into
base: main
Choose a base branch
from

Conversation

wbernoudy
Copy link
Member

@wbernoudy wbernoudy commented Nov 14, 2024

Adds InputNode which subclasses ConstantNode but allows assigning of the state after initialization.

This is to support the new NaryReduceNode, which does an n-ary reduce operation on an arbitrary number of inputs/predecessors. The node is constructed by providing an expression, formulated as a separate Graph, and initial values for the reduction operation.

The current design accepts any Graph but then throws exceptions if it contains unsupported nodes. It would be best if we can pass along helpful error messages to users at the python level. Currently this works by encoding the error message and a Node* in a simple JSON string that is then parsed from Python, where we can grab the corresponding symbol.

@wbernoudy
Copy link
Member Author

I tried out some more complicated things to support custom exception handling in Cython. I put it in a separate branch for now as I'm not sure we'll go that direction. Separate PR here to try to make it a bit easier to review wbernoudy#1

Copy link
Member

@arcondello arcondello left a comment

Choose a reason for hiding this comment

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

Mostly looked at design rather than line-by-line review.

dwave/optimization/src/array.cpp Show resolved Hide resolved
class NaryReduceNode : public ArrayOutputMixin<ArrayNode> {
public:
// Runtime constructor that can be used from Cython/Python
NaryReduceNode(Graph&& expression, const std::vector<InputNode*>& inputs,
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if it makes more sense to not have the user provide inputs etc explicitly, but rather to inspect the expression. Maybe set_objetive() sets the output? And we can traverse the nodes looking for InputNode

Copy link
Member Author

Choose a reason for hiding this comment

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

I think inputs should be provided explicitly because they need to be mapped to the operands/predecessors. Otherwise we are relying on the implicit ordering of when the nodes are added to the expression model, and it becomes less clear which one is the "special" input that takes the previous value.

But I still find this interface confusing... was thinking about taking a map between inputs/operands as well.

Copy link
Member

Choose a reason for hiding this comment

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

At the Python level this could perhaps be addressed by not adding an input() method, but having a subclass of Model that requires num_inputs at construction-time. Then we rely on the order? We might have to treat inputs as decision variables in that they get topologically ordered immediately in that case... Needs thought.

Copy link
Member Author

Choose a reason for hiding this comment

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

I like that. The ExpressionModel or whatever (subclass of Model) could disallow adding decisions.

Perhaps we could also somehow pull a list of supported nodes from C++ and then disallow adding unsupported symbols to ExpressionModel. This will go a long way to prevent constructing unsupported expressions, as I don't think we will have any supported nodes that can have non-scalar output given all scalar inputs.

Copy link
Member Author

Choose a reason for hiding this comment

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

And I'll note I expect the standard flow when creating expressions is just to create a list of Inputs anyway and use that. So exp = ExpressionModel(7), exp.inputs[0] + exp.inputs[1] feels natural to me.

Copy link
Member

Choose a reason for hiding this comment

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

Given that we're now using objective for the output, and expression.input() to create the inputs, IMO we should just read the inputs() and objective() off the expression rather than providing them explicitly. We can always allow reorderings etc via a method like this later.

dwave/optimization/include/dwave-optimization/utils.hpp Outdated Show resolved Hide resolved
dwave/optimization/include/dwave-optimization/utils.hpp Outdated Show resolved Hide resolved
dwave/optimization/model.py Outdated Show resolved Hide resolved
dwave/optimization/src/nodes/constants.cpp Outdated Show resolved Hide resolved
dwave/optimization/src/nodes/lambda.cpp Outdated Show resolved Hide resolved
dwave/optimization/src/nodes/lambda.cpp Outdated Show resolved Hide resolved
dwave/optimization/src/nodes/lambda.cpp Outdated Show resolved Hide resolved
dwave/optimization/include/dwave-optimization/array.hpp Outdated Show resolved Hide resolved
dwave/optimization/src/array.cpp Outdated Show resolved Hide resolved
dwave/optimization/src/nodes/lambda.cpp Outdated Show resolved Hide resolved
@wbernoudy wbernoudy marked this pull request as ready for review December 11, 2024 18:30
@@ -73,6 +74,8 @@ class Graph {
public:
Graph();
~Graph();
Graph(Graph&&);
Graph& operator=(Graph&& other) = default;
Copy link
Member

Choose a reason for hiding this comment

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

For consistency, we should put this in the .cpp with the others. Or move the others into the .hpp.

Copy link
Member

Choose a reason for hiding this comment

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

Nevermind! See #184

@@ -185,7 +186,8 @@ cdef class _Graph:
objective_id = json.loads(objective_buff)
if not isinstance(objective_id, int) or objective_id >= model.num_nodes():
raise ValueError("objective must be an integer and a valid node id")
model.minimize(symbol_from_ptr(model, model._graph.nodes()[objective_id].get()))
model._set_objective(symbol_from_ptr(model, model._graph.nodes()[objective_id].get()))
# model.minimize(symbol_from_ptr(model, model._graph.nodes()[objective_id].get()))
Copy link
Member

Choose a reason for hiding this comment

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

leftover comment

Comment on lines 264 to 267
self.lock()
self.into_file(file, max_num_states=max_num_states, only_decision=only_decision)
self.unlock()
return
Copy link
Member

Choose a reason for hiding this comment

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

Why this change?

Copy link
Member Author

Choose a reason for hiding this comment

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

The context manager version of .lock() is defined only for Model currently. I could also add it to Expression or _Graph.

Copy link
Member

Choose a reason for hiding this comment

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

Ah, excellent point! In which case we should at least do it in a

self.lock()
try:
    self.into_file(...)
finally:
    self.unlock()

Copy link
Member Author

Choose a reason for hiding this comment

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

Yep, that's better!

dwave/optimization/_model.pyx Show resolved Hide resolved
>>> model.minimize(y)
"""
def _set_objective(self, ArraySymbol value):
"""Set the objective value on the ``dwave::optimization::Graph``."""
Copy link
Member

Choose a reason for hiding this comment

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

This might be a good place to note that we are using "objective" somewhat generously, and that it can refer to the output of an expression as well.

dwave/optimization/_model.pyx Show resolved Hide resolved
class NaryReduceNode : public ArrayOutputMixin<ArrayNode> {
public:
// Runtime constructor that can be used from Cython/Python
NaryReduceNode(Graph&& expression, const std::vector<InputNode*>& inputs,
Copy link
Member

Choose a reason for hiding this comment

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

Given that we're now using objective for the output, and expression.input() to create the inputs, IMO we should just read the inputs() and objective() off the expression rather than providing them explicitly. We can always allow reorderings etc via a method like this later.

dwave/optimization/include/dwave-optimization/utils.hpp Outdated Show resolved Hide resolved
dwave/optimization/libcpp/graph.pxd Outdated Show resolved Hide resolved
dwave/optimization/model.py Outdated Show resolved Hide resolved
@wbernoudy wbernoudy force-pushed the feature/nary-reduce-node branch from 69bbd65 to be9f5ee Compare December 13, 2024 04:07
dwave/optimization/_model.pyi Outdated Show resolved Hide resolved
dwave/optimization/symbols.pyx Outdated Show resolved Hide resolved
tests/cpp/nodes/test_constants.cpp Outdated Show resolved Hide resolved
`NaryReduce`).
Args:
expression:
Copy link
Member

@arcondello arcondello Dec 16, 2024

Choose a reason for hiding this comment

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

I just noticed that numpy.ufunc.reduce(), functools.reduce() and std::accumulate() all use the left-most argument for the init/accumulated value. So I think we should follow that convention by making it the first input.

For completeness std::reduce() requires commutative and associative, which we definitely don't want to require.

@wbernoudy wbernoudy force-pushed the feature/nary-reduce-node branch from 3a45e03 to f60e6d7 Compare January 10, 2025 19:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants