Skip to content

Fix DSVAE's forward path and the arguments it drops - #8413

Open
vineethsaivs wants to merge 1 commit into
deepspeedai:masterfrom
vineethsaivs:fix/dsvae-forward-and-decode
Open

Fix DSVAE's forward path and the arguments it drops#8413
vineethsaivs wants to merge 1 commit into
deepspeedai:masterfrom
vineethsaivs:fix/dsvae-forward-and-decode

Conversation

@vineethsaivs

Copy link
Copy Markdown
Contributor

What breaks

DSVAE is the wrapper deepspeed.init_inference swaps in for a diffusers VAE, so every Stable Diffusion encode/decode goes through it. Three things in it do not match what they wrap.

1. forward reads a flag DSVAE never sets.

AttributeError: 'DSVAE' object has no attribute 'cuda_graph_created'

__init__ sets decoder_cuda_graph_created, encoder_cuda_graph_created and all_cuda_graph_created; _create_cuda_graph sets all_cuda_graph_created. forward tests self.cuda_graph_created, which only DSUNet has. enable_cuda_graph defaults to True, so that is the default path, and all_cuda_graph_created is written twice and read nowhere.

2. _forward is a copy of DSUNet._forward.

def _forward(self, sample, timestamp, encoder_hidden_states, return_dict=True):
    return self.vae(sample, timestamp, encoder_hidden_states, return_dict)

A VAE has no timestep and no encoder hidden states. AutoencoderKL.forward is (sample, sample_posterior=False, return_dict=True, generator=None), so DSVAE(sample) raises TypeError: missing 2 required positional arguments, and satisfying it lands timestamp on sample_posterior, encoder_hidden_states on return_dict, and return_dict on generator.

3. _decode accepts generator and drops it.

StableDiffusionPipeline.__call__ calls self.vae.decode(latents / scaling_factor, return_dict=False, generator=generator). AutoencoderKL ignores it, but ConsistencyDecoderVAE and AutoencoderTiny sample with it, so their decode is silently non-reproducible behind init_inference.

What changed

  • forward reads all_cuda_graph_created, the flag the class sets. That makes the dead flag live and the default path work.
  • _forward takes AutoencoderKL.forward's parameters and passes them by keyword.
  • _decode forwards generator when one was given. Only when given, since AutoencoderKLTemporalDecoder.decode has no such argument.

Test

New tests/unit/inference/test_ds_vae.py, three plain functions driving DSVAE with a recording stub whose signatures are copied from AutoencoderKL. No accelerator, no model download.

3 failed before, 3 passed after (AttributeError, TypeError, and generator arriving as None), run on torch 2.11.0 against deepspeed 0.19.6, whose vae.py is byte-identical to master.

yapf --style .style.yapf --diff clean on both files.

DSVAE.forward tests self.cuda_graph_created, which DSVAE never sets. __init__
sets decoder_cuda_graph_created, encoder_cuda_graph_created and
all_cuda_graph_created, and _create_cuda_graph sets all_cuda_graph_created, so
with the default enable_cuda_graph=True the first call raises
AttributeError: 'DSVAE' object has no attribute 'cuda_graph_created'.
all_cuda_graph_created is written twice and read nowhere.

_forward is a copy of DSUNet._forward: it requires timestamp and
encoder_hidden_states, which a VAE does not take, and passes them positionally
into AutoencoderKL.forward(sample, sample_posterior, return_dict, generator).
Take the parameters that method actually has and pass them by keyword.

_decode accepts generator and drops it. Every diffusers pipeline calls
vae.decode(latents, return_dict=False, generator=generator), and a stochastic
decoder such as ConsistencyDecoderVAE samples with it. Forward it when one is
given; not every VAE's decode takes the argument.

Signed-off-by: Vineeth Sai <vineethsai4444@gmail.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4849081cf5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

# VAE's `decode` accepts the argument.
if generator is None:
return self.vae.decode(x, return_dict=return_dict)
return self.vae.decode(x, return_dict=return_dict, generator=generator)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Refresh generators before CUDA graph replay

With CUDA graphs enabled, the first decode call stores this torch.Generator in static_decoder_kwargs, while _graph_replay_decoder updates only tensor kwargs before replaying the already captured graph. A later call with a different generator therefore still samples using the first call's generator, so stochastic decoders produce results determined by an earlier request rather than the caller's seed; the same non-tensor-capture issue is introduced for forward's generator argument. Exclude generator-bearing calls from graph replay or rebuild/refresh the graph when the generator changes.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The container half is real and is now #8488: _graph_replay_decoder matches on torch.is_tensor, so anything nested is never refreshed. That helper walks dicts, lists and tuples at all six replay sites.

The generator itself is a different matter and I am leaving it alone here. A torch.Generator is not a tensor, so there is nothing to copy into, and a CUDA graph cannot capture RNG state that way at all. Forwarding it, which is what this PR does, is what the pipelines need for the non-graph path. Making seeded decodes correct under capture needs the graph pool to take a generator, which is a bigger change than this PR.

Comment on lines +59 to +62
ds_vae = DSVAE(vae, enable_cuda_graph=False)
generator = torch.Generator()

ds_vae.decode(torch.zeros(1), return_dict=False, generator=generator)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Add an actual-device inference integration test

This test disables CUDA graphs and uses a CPU recording stub, so it never exercises the inference path changed by this commit under CUDA capture/replay. In particular, it cannot detect generator or argument behavior once the VAE has been graph-captured; add and run an actual-device integration test using a real VAE/pipeline for this inference-level contract.

AGENTS.md reference: AGENTS.md:L35-L36

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not adding one. A real VAE under CUDA capture needs a GPU, and I do not have one, so I would be adding a test I cannot run.

What this PR changes is which arguments DSVAE hands to the wrapped VAE. The recording stub pins exactly that, and it fails on master. The capture path is unchanged by this diff.

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.

1 participant