Skip to content

[BugFix] Fix slice sampler, CQL Lagrange and multi-agent export correctness bugs - #4700

Merged
theap06 merged 6 commits into
pytorch:mainfrom
theap06:fix/v015-data-losses-correctness
Nov 7, 2026
Merged

theap06 merged 6 commits into
pytorch:mainfrom
theap06:fix/v015-data-losses-correctness

Conversation

@theap06

@theap06 theap06 commented Nov 6, 2026 •

Copy link
Copy Markdown
Collaborator

Description

Fixes silent wrong results in the replay samplers, CQLLoss and multi-agent export. Each fix has a regression test that fails on main.

  • PrioritizedSliceSampler now marks slice starts with is_init, like SliceSampler, so recurrent policies get correct sequence boundaries. Adds a PSS case to the replay buffer benchmarks.
  • SliceSampler with a right span no longer produces zero-length slices or IndexError.
  • PrioritizedSliceSampler with slice_len=1 can start on every step (it raised or only drew first steps).
  • CQLLoss(with_lagrange=True) matches the reference CQL(H) Lagrange update: loss_alpha_prime no longer sends gradients into the Q networks, loss_cql is scaled by alpha_prime, and reduction="none" works.
  • MultiAgentMLP / MultiAgentConvNet with share_params=True are no longer corrupted by a non-strict torch.export.

Breaking changes

  • With with_lagrange=True (used by both SOTA CQL configs), loss_cql values and gradients change, so curves are not comparable with earlier runs.
  • When the stored data has is_init, sampled PrioritizedSliceSampler batches now set it at every slice start.

Motivation and Context

These produce wrong training inputs, wrong gradients or a broken model without an error.

  • I have raised an issue to propose this change (required for new features and bug fixes)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • Breaking change (fix or feature that would cause existing functionality to change)

Checklist

  • I have read the CONTRIBUTION guide (required)
  • My change requires a change to the documentation.
  • I have updated the tests accordingly (required for a bug fix or a new feature).
  • I have updated the documentation accordingly.

PrioritizedSliceSampler accepted init_key and documented that it marks the
first step of every slice, but its sample method built the truncated, done
and terminated entries itself and never wrote init_key. Recurrent learners
fed by a prioritized slice buffer therefore did not restart their hidden
state at slice starts.

The sampler now finalizes its index through SliceSampler._finalize_index,
so it writes the same truncated, done, terminated and is_init entries as
SliceSampler. The wrapped-buffer fuzz test now stores is_init and checks
that every slice starts with it and has no other episode start.
With span=True or span=(..., True), SliceSampler drew relative starts in
[start_point, length], so a start could land one step past the end of its
trajectory. That slice had zero length: it was silently dropped, returning
fewer slices than requested, or made the truncated marker index out of
range. The exclusive upper bound is now the trajectory length, so the
latest start is the last step and every slice keeps at least one step.
PrioritizedSliceSampler zeroes the priority of the last
slice_len - 1 - span_right steps of every trajectory with
pad[:, -num_forbidden:]. When num_forbidden is 0 (slice_len=1, or a right
span of slice_len - 1) this slice became pad[:, 0:] and forbade every step.
With strict_length=True sampling raised "non-positive p_sum"; with
strict_length=False only the first step of each trajectory could be drawn.
The slice now starts at width - num_forbidden, clamped at 0, so a
non-positive count forbids nothing.
With with_lagrange=True, CQLLoss departed from the reference implementation
(aviralkumar2907/CQL) in three ways:

- loss_alpha_prime was built from the undetached conservative terms, so
  backpropagating it also pushed gradients into the Q-network parameters.
- loss_cql was not scaled by alpha_prime. The reference trains the
  Q-networks on alpha_prime * (c - lagrange_thresh) with alpha_prime held
  fixed, and trains alpha_prime only through
  -alpha_prime * ((c_1 + c_2) / 2 - lagrange_thresh).
- reduction="none" raised a batch-dimension mismatch because
  loss_alpha_prime was always reduced to a scalar.

loss_cql is now alpha_prime.detach() * (c_1 + c_2 - 2 * lagrange_thresh),
and loss_alpha_prime uses detached conservative terms and follows the loss
reduction, so it is per-sample under reduction="none". Summing all losses
into one backward pass, as the sota-implementations CQL scripts do, now
gives each parameter group the reference gradient.

This changes loss_cql values for with_lagrange=True users (alpha_prime
scaling and the threshold offset) and the gradients both losses produce.
Users without the Lagrange multiplier are not affected.
…strict export

Exporting a MultiAgentMLP or MultiAgentConvNet built with share_params=True
through torch.export.export(strict=False), the default, corrupted the eager
network. In a non-strict trace tensordict's to_module takes its compile path,
and its restore writes the meta placeholders of the template network into
_parameters, while export puts the same placeholders back in the module
__dict__. Afterwards the state_dict carries extra _empty_net.* meta entries,
so a checkpoint saved after export no longer loads strictly into a fresh
network, and eager forward reads the placeholders: MultiAgentMLP raises a
meta device error and MultiAgentConvNet silently returns garbage, so eager
and exported outputs disagree.

During a non-strict export the shared branch now calls the template with
torch.func.functional_call, with the placeholders taken out of the template
for the call and put back afterwards. Eager, compiled and strict-export runs
keep using to_module, and the parameter layout is unchanged.
@theap06 theap06 added the ci/olddeps Run the tests-olddeps suite (oldest supported torch) on this PR label Nov 6, 2026
@pytorch-bot

pytorch-bot Bot commented Nov 6, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/rl/4700

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 2d52da0 with merge base c5a5e3a (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Nov 6, 2026
PrioritizedSliceSampler with truncated_key=None now finishes through
SliceSampler._finalize_index, whose no-truncation branch gathered every key of
the sample just to read is_init. On 84x84 pixels that made rb.sample 2.7x
slower than main. The marker now reads the single init_key entry from the
underlying TensorDict and skips the read when init_key is None. Adds a
PrioritizedSliceSampler case to the replay buffer sample benchmark.
@github-actions github-actions Bot added the Benchmarks rl/benchmark changes label Nov 7, 2026
@theap06
theap06 marked this pull request as ready for review November 7, 2026 07:11
@theap06
theap06 merged commit 713e293 into pytorch:main Nov 7, 2026
127 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Benchmarks rl/benchmark changes BugFix ci/olddeps Run the tests-olddeps suite (oldest supported torch) on this PR CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Integrations/torch_geometric Integrations Modules Objectives ReplayBuffers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant