Skip to content

Fix PyTorch DQN and NFSP checkpointing - #1620

Open
ArchawinL wants to merge 2 commits into
google-deepmind:masterfrom
ArchawinL:fix-pytorch-dqn-nfsp-checkpointing
Open

ArchawinL wants to merge 2 commits into
google-deepmind:masterfrom
ArchawinL:fix-pytorch-dqn-nfsp-checkpointing

Conversation

@ArchawinL

@ArchawinL ArchawinL commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

In v2.0.2 and on master, pytorch.dqn.DQN.load and pytorch.nfsp.NFSP.save/restore fail on every call.

Reproduction (v2.0.2):

import pathlib, tempfile
from open_spiel.python.pytorch import dqn, nfsp

tmp = pathlib.Path(tempfile.mkdtemp())

agent = dqn.DQN(0, state_representation_size=11, num_actions=2)
agent.save(tmp / "dqn.pt")
agent.load(tmp / "dqn.pt")
# KeyError: 'model_state_dict'

agent = nfsp.NFSP(0, state_representation_size=11, num_actions=2,
                  hidden_layers_sizes=[16], reservoir_buffer_capacity=10,
                  anticipatory_param=0.1)
agent.save(tmp / "nfsp")
# RuntimeError: Parent directory .../nfsp does not exist.
# If the directory exists: RuntimeError: File .../nfsp cannot be opened.
# With a str path: TypeError: unsupported operand type(s) for /: 'str' and 'str'

Cause (introduced in #1419):

  1. save writes the keys model/optimiser, but load/restore read model_state_dict/optimizer_state_dict.
  2. NFSP.save uses checkpoint_dir both as a directory (for the inner DQN) and as a file (for torch.save).

Fix:

  • DQN.load reads the keys save writes. save now also stores the target network, and load restores it, so resuming between target updates no longer resets it. Files written by v2.0.2 still load; they never stored the target, so it starts from the online network.
  • NFSP treats checkpoint_dir as a directory: it creates the directory, writes q_network.pt (inner DQN) and avg_network.pt (average network), and accepts str paths as the JAX version does.
  • nfsp_leduc_pytorch.py now checkpoints each player to its own subdirectory. Before, both players shared one path.

I chose this NFSP layout; happy to change it.

Tests: new save→load/restore round-trip tests in dqn_pytorch_test.py and nfsp_pytorch_test.py, including the target networks. Failed before this change and now passing.

Not included: saving the replay and reservoir buffers.

DQN.load and NFSP.restore read the keys "model_state_dict" and
"optimizer_state_dict", but save writes "model" and "optimiser", so
loading always raised KeyError. NFSP.save also used checkpoint_dir as
both a directory (for the inner DQN) and a file (for torch.save), so it
always raised RuntimeError.

DQN.load now reads the keys save writes, which leaves the file format
unchanged. NFSP.save creates checkpoint_dir and writes q_network.pt and
avg_network.pt into it; restore reads the same files. Both accept str
paths. nfsp_leduc_pytorch.py now checkpoints each player to its own
subdirectory.

Adds save/load round-trip tests for DQN and NFSP.

@sylvesterkaczmarek sylvesterkaczmarek 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.

The DQN checkpoint still does not preserve the target network. save() writes only _q_network, and load() copies that state into both networks, so resuming between target updates silently resets the lagged target to the online network and changes subsequent learning. Could the target state be saved/restored separately, with the regression comparing the restored target to the original _target_q_network?

DQN.save wrote only the online Q-network and load copied it into both
networks, so resuming between target updates reset the lagged target
network and changed subsequent learning. This also affected the inner
DQN of NFSP.

save now stores the target network under "target_model" and load
restores it. Checkpoints without that key, such as those written by
v2.0.2, fall back to the previous behaviour.
@ArchawinL

Copy link
Copy Markdown
Contributor Author

Fair point. Tests are updated, and save now also stores the target network.

One related gap I've left out of this PR: pytorch.boltzmann_dqn.BoltzmannDQN inherits DQN.save/load, which don't include its _prev_q_network. After a load, it keeps the fresh agent's initial weights, so a restored agent doesn't evaluate like the saved one.

It's not visible right now because _boltzmann_action_probs passes the legal action indices to torch.BoolTensor as if they were a mask (on Kuhn, [0, 1] becomes [False, True]), so the previous network never affects the evaluation policy. Fixing the checkpoint only makes sense together with that, and it needs a way for the subclass to add to the checkpoint. Happy to open a PR after this one if that sounds right.

@sylvesterkaczmarek sylvesterkaczmarek 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.

Rechecked current b8aa626. DQN checkpoints now store the target network separately, load restores it when present, and older checkpoints fall back to the online model for compatibility. The regression directly compares the restored target state to the original lagged target network. This resolves the learning-state reset I raised.

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.

2 participants

⌂ Proxy