Skip to content

Implement state checkpointing for decision nodes - #587

Open
arcondello wants to merge 12 commits into
dwavesystems:mainfrom
arcondello:feature/decision-checkpoints
Open

Implement state checkpointing for decision nodes#587
arcondello wants to merge 12 commits into
dwavesystems:mainfrom
arcondello:feature/decision-checkpoints

Conversation

@arcondello

@arcondello arcondello commented Jul 17, 2026

Copy link
Copy Markdown
Member

Closes #510
Closes #552 by replacing it

AI Generation Disclosure

No AI was used to write the code. I intend to use it to review this PR.

@arcondello arcondello added the enhancement New feature or request label Jul 17, 2026
@arcondello
arcondello force-pushed the feature/decision-checkpoints branch from 6df7006 to 953304e Compare July 17, 2026 23:25

CollectionCheckpoint_::CollectionCheckpoint_(CollectionStateData_* state_ptr) :
updates_(),
drop_(state_ptr->all_updates_.size()), // so we ignore any updates added before we're made

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What happens if some mutations are made to the node, then you make a checkpoint (causing the drop_ to be set > 0), and then you revert, make more mutations, and commit? Won't this cause some updates to be "dropped" that are actually relevant to the checkpoint?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, I think I mean "make another checkpoint" as final step in the example rather than commit.

@arcondello arcondello Jul 20, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yup! Good catch. Fixed in 214c5e2

@arcondello
arcondello force-pushed the feature/decision-checkpoints branch 2 times, most recently from b4679c2 to 214c5e2 Compare July 20, 2026 18:59
This avoids using GCC11 which had some bugs in their ranges
implementation.
@arcondello
arcondello force-pushed the feature/decision-checkpoints branch from 86b97e8 to 9cd035b Compare July 20, 2026 19:18
Comment thread dwave/optimization/include/dwave-optimization/state.hpp Outdated
@arcondello
arcondello force-pushed the feature/decision-checkpoints branch 2 times, most recently from f1aa563 to 06b9dc5 Compare July 21, 2026 21:20
@arcondello
arcondello force-pushed the feature/decision-checkpoints branch from 06b9dc5 to f70699e Compare July 21, 2026 21:21
@arcondello
arcondello force-pushed the feature/decision-checkpoints branch from 30637e6 to 5fbe15b Compare July 27, 2026 18:46
@arcondello arcondello changed the title Implement state checkpointing for CollectionNode Implement state checkpointing for decision nodes Jul 27, 2026

// Overloads required by the DecisionNode ABC *****************************

// DynamicArrayTestingNode does not impement checkpointing

@arcondello arcondello Jul 27, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See #593

Also I see the typo here.

@arcondello
arcondello force-pushed the feature/decision-checkpoints branch from 5fbe15b to 4402568 Compare July 27, 2026 18:51
@arcondello
arcondello marked this pull request as ready for review July 27, 2026 18:51
@arcondello
arcondello requested a review from fastbodin July 27, 2026 18:52
}
}

void Graph::propose(State& state) const {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Could be smarter with #589 or #592, but that's for another PR.


set_ptr->assign_from_checkpoint(state, std::move(checkpoint0));

// CHECK_THAT(set_ptr->view(state), RangeEquals({0, 1}));

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

oops, will restore

}
}

// TODO: within one propagation

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

this is done, need to remove this comment

auto checkpoint = ptr->checkpoint(state); // 0, 1, 0, 1, 0, 1, 0, 1, 0, 1

THEN("We can mutate and then assign from that checkpoint") {
ptr->set_value(state, 0, 1); // 0, 1, 0, 1, 0, 1, 0, 1, 0, 1

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

// 1, 1, 0, 1, 0, 1, 0, 1, 0, 1

since you called set_value()

ptr->exchange(state, 0, 2); // [-2, -4, -4, -2, 0, 0, 2, 2, 4, 4]
ptr->set_value(state, 3, 1); // [-2, -4, -4, 1, 0, 0, 2, 2, 4, 4]

THEN("We can commit, then assign from the checkpoint") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit:

                THEN("We can propose, then assign from the checkpoint") {

I realize propose is propagate and commit. As this is a Nit, I am not going to comment at all duplicate instances.

AND_WHEN("We create a checkpoint to that state") {
auto checkpoint = ptr->checkpoint(state); // 0, 1, 0, 1, 0, 1, 0, 1, 0, 1

THEN("We can mutate and then assign from that checkpoint") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Nit

                THEN("We can mutate, then propose, and then assign from that checkpoint") {

CHECK_THAT(inode_ptr->view(state), RangeEquals({8, 8, 8, 8, 8, 8, -3, 3}));
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

A bit of a pain, but I think it might be worth adding a single test for BinaryNode with checkpointing to check that all the correct indices are cached with 1s and 0s (I am referring to the BinaryNode DisjointSparseSet).

THEN("Sum constraint sums, tracked indices, and state are correct") {
    CHECK(bnode_ptr->sum_constraints_lhs(state).size() == 1);
    CHECK(bnode_ptr->sum_constraints_lhs(state).data()[0].size() == 1);
    CHECK_THAT(bnode_ptr->sum_constraints_lhs(state)[0], RangeEquals({3}));
    check_indices<true>(state, bnode_ptr, 0, 0, {1, 2, 5}); // <- ***** This check is the important one ******
    check_indices<false>(state, bnode_ptr, 0, 0, {0, 3, 4}); // <- ***** And this one ******
    CHECK_THAT(bnode_ptr->view(state), RangeEquals(expected_init));
}

Comment thread dwave/optimization/src/nodes/numbers.cpp
const auto& [idx, old, _] :
state_data->diff() | std::views::reverse | std::views::take(excess_updates)
) {
state_data->set(idx, old);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I would just add a note that this by-passes (currently) the sum constraint lhs tracking.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking at this again, can you remind we why this is fine?

assert(size_ >= 0 && static_cast<std::size_t>(size_) == buffer.size());
}

// Commit the changes and clear the diff by returning the diff buffer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Confused where the commit() is and am feeling like I am missing something.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Commiting the changes just means keeping the status quo 😄

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wait. Given a committed state, if we then make a series of changes (and create a diff) this method detaches the diff. Is this considered a silent commit()? The state is still modified but now we don't have the diff to now how it was modified?

size_ = buffer.size();
}

// Commit the changes and clear the diff by returning the diff buffer.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same doc string comments as commit_and_detach(). Intentional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Should say "revert"

Comment thread dwave/optimization/src/nodes/_checkpoints.cpp
Comment thread dwave/optimization/src/nodes/_checkpoints.cpp
Comment thread dwave/optimization/src/nodes/_checkpoints.hpp
if (auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_)) {
prev_ptr->commit_updates(std::vector<Update>(diff.begin(), diff.end()));
assert(prev_ptr->drop_ == 0);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

To be clear, we are doing the static_cast because the prev_ptr_ member is a LinkedListCheckpoint which does not have the method commit_updates(). Functionally, the only reason the static_cast would return a nullptr is if prev_ptr_ is a nullptr already.

I am just comparing this to the destructor which checks if (prev_ptr_ == nullptr) and does not have a if around static_cast<DiffCheckpoint*>(prev_ptr_). It feels like this should either be

    if (auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_)) {
        prev_ptr->commit_updates(std::vector<Update>(diff.begin(), diff.end()));
        assert(prev_ptr->drop_ == 0);
        return;
    }
    assert(prev_ptr_ == nullptr);

or should resemble the destructor

    if (prev_ptr_ == nullptr) return;

    auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_);
    assert(prev_ptr); // <- I added this
    prev_ptr->commit_updates(std::vector<Update>(diff.begin(), diff.end()));
    assert(prev_ptr->drop_ == 0);

if (prev_ptr_ == nullptr) return;

// otherwise we need to transfer our info over
auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

See comment above. If we keep this structure, I would consider adding

assert(prev_ptr);

// otherwise we need to transfer our info over
auto* prev_ptr = static_cast<DiffCheckpoint*>(prev_ptr_);
assert(prev_ptr->drop_ == 0);
for (auto& updates : updates_) prev_ptr->commit_updates(std::move(updates));

@fastbodin fastbodin Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The reason we do not do

prev_ptr->commit_updates(this->detach_updates());

is because drop_ (which I would love a doc string for 😄) says how many of the vectors in vector<vector<Updates>> we drop?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

how many updates of the last vector. And agree drop_ needs a docstring, @alexzucca90 had the same feedback

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The last vector! Okay. In that case, why do the loop as opposed to

prev_ptr->commit_updates(this->detach_updates());

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

prev_ptr->commit_updates() takes a single diff (usually from the node state). Here we want to transfer a sequence of diffs. We could flatten, or add an overload I suppose..

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It does not make a difference really to me. Just trying to understand. I had thought that

    auto detach_updates() {
        auto updates = std::move(updates_) | std::views::join;
        assert(updates_.empty());
        return updates;
    }

would result in a std::vector<Update> since updates_ is a std::vector<std::vector<Update>>. I had not thought you would need an overload?

@arcondello

Copy link
Copy Markdown
Member Author

I think I need to take another documentation pass. I'll try to do that today

@fastbodin fastbodin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Officially gone through everything. I need to look at the number.cpp and collections.cpp again. On the whole, looks good. Most of my comments above are clarifications. I will resolved them ASAP to avoid clogging the thread on this PR.

// Right now, you can only revert to the most recent checkpoint. It's
// pretty straightforward to support going further back, but this is all
// we need right now.
assert(this->checkpoint_ptr<CollectionCheckpoint_>() == checkpoint_ptr);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Took me a hot minute to realize this was templated from

class CheckpointableState {
 protected:
    template <std::derived_from<LinkedListCheckpoint> T>
    T* checkpoint_ptr() {
        return static_cast<T*>(prev_ptr_);
    }

Comment on lines +443 to +444
// DisjointBitSetsNode is on the way out, so let's do the simplest possible
// implementation for now.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Agreed.

State& state,
std::unique_ptr<NodeStateCheckpoint>& checkpoint
) const {
const DisjointBitSetsCheckpoint_* checkpoint_ptr =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

const auto*?

check.reset();
}
}
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I love this test and wish there was a final CHECK() we could do :rofl


auto* checkpoint_ptr = static_cast<NumberNodeCheckpoint_*>(checkpoint.get());

// todo: assert that this checkpoint is the latest

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Testing my understanding. You mean

        // Right now, you can only revert to the most recent checkpoint. It's
        // pretty straightforward to support going further back, but this is all
        // we need right now.
        assert(this->checkpoint_ptr<NumberNodeCheckpoint_>() == checkpoint_ptr);

const auto& [idx, old, _] :
state_data->diff() | std::views::reverse | std::views::take(excess_updates)
) {
state_data->set(idx, old);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looking at this again, can you remind we why this is fine?

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.

Add checkpointing method(s) for decision array nodes

3 participants