-
Notifications
You must be signed in to change notification settings - Fork 34.7k
Fix vibevoice TTS batched audio index #48902
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
Merged
ebezzam
merged 11 commits into
huggingface:main
from
ebezzam:fix-vibevoice-batched-audio-index
Sep 21, 2026
+97
−19
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
42d7351
Fix VibeVoice batched generation attributing audio to the wrong sequence
ebezzam 0688025
CLean up.
ebezzam b585887
Nit.
ebezzam f7c0e61
nit.
ebezzam 1dd327f
Remove guard
ebezzam cef4d5c
address some comments
ebezzam 3a04c37
Merge branch 'fix-vibevoice-batched-audio-index' of github.com:ebezza…
ebezzam 7b63337
split test
ebezzam 608bc0b
Follow two line pattern of other tests
ebezzam 44e912e
remove rng
ebezzam 1400d2f
add seed to avoid flakiness
ebezzam File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,8 +14,10 @@ | |
|
|
||
| import copy | ||
| import json | ||
| import random | ||
| import unittest | ||
| from pathlib import Path | ||
| from unittest.mock import patch | ||
|
|
||
| import pytest | ||
|
|
||
|
|
@@ -46,14 +48,7 @@ | |
|
|
||
|
|
||
| class DummyNoiseScheduler: | ||
| """ | ||
| A simple dummy noise scheduler for testing purposes. | ||
|
|
||
| Contrary to real schedulers, `step` returns a *deterministic* output that does not depend on the (randomly | ||
| sampled) input latent. The denoised latent is fed back into the language model as the next-step embedding, so a | ||
| random latent would make generated sequences differ between two `generate` calls (the global RNG state advances), | ||
| breaking tests that compare two runs (e.g. dynamic vs static cache, eager vs compiled). | ||
| """ | ||
| """A simple dummy noise scheduler for testing purposes.""" | ||
|
|
||
| def __init__(self): | ||
| self.num_inference_steps = None | ||
|
|
@@ -65,9 +60,8 @@ class StepOutput: | |
| def __init__(self, prev_sample): | ||
| self.prev_sample = prev_sample | ||
|
|
||
| # Deterministic output: ignore the random input latent and noise estimate (see class docstring) | ||
| prev_sample = torch.zeros_like(sample) + 0.1 * timestep.to(sample.dtype) / 1000 | ||
| return StepOutput(prev_sample) | ||
| # Simple update | ||
| return StepOutput(sample - 0.1 * eps) | ||
|
|
||
| def set_timesteps(self, num_inference_steps): | ||
| self.num_inference_steps = num_inference_steps | ||
|
|
@@ -141,10 +135,10 @@ def __init__( | |
| self.num_hidden_layers = text_config["num_hidden_layers"] | ||
| self.pad_token_id = text_config["pad_token_id"] | ||
|
|
||
| def get_config(self): | ||
| def get_config(self, audio_config=None): | ||
| return VibeVoiceConfig( | ||
| text_config=self.text_config, | ||
| audio_config=self.audio_config, | ||
| audio_config=audio_config if audio_config is not None else self.audio_config, | ||
| semantic_model_config=self.semantic_model_config, | ||
| diffusion_head_config=self.diffusion_head_config, | ||
| use_cache=self.use_cache, | ||
|
|
@@ -155,10 +149,12 @@ def get_config(self): | |
| audio_token_id=5, # Instead of default 151654 | ||
| ) | ||
|
|
||
| def prepare_config_and_inputs(self): | ||
| config = self.get_config() | ||
| input_ids = ids_tensor([self.batch_size, self.seq_length], self.vocab_size) | ||
| attention_mask = torch.ones([self.batch_size, self.seq_length], dtype=torch.long, device=torch_device) | ||
| def prepare_config_and_inputs(self, batch_size=None, seq_length=None, rng=None, audio_config=None): | ||
| batch_size = batch_size if batch_size is not None else self.batch_size | ||
| seq_length = seq_length if seq_length is not None else self.seq_length | ||
| config = self.get_config(audio_config=audio_config) | ||
| input_ids = ids_tensor([batch_size, seq_length], self.vocab_size, rng=rng) | ||
| attention_mask = torch.ones([batch_size, seq_length], dtype=torch.long, device=torch_device) | ||
| return config, input_ids, attention_mask | ||
|
|
||
| def prepare_config_and_inputs_for_common(self): | ||
|
|
@@ -178,6 +174,50 @@ def create_and_check_model(self, config, input_ids, attention_mask): | |
| self.parent.assertIsNotNone(result.logits) | ||
| self.parent.assertEqual(result.logits.shape, (self.batch_size, self.seq_length, self.vocab_size)) | ||
|
|
||
| def create_and_check_batched_matches_single(self, config, input_ids, attention_mask, use_cache=True): | ||
| # Fixed weights, so that the decoded audio is reproducible across runs. | ||
| set_seed(7) | ||
| model = VibeVoiceForConditionalGeneration(config=config).to(torch_device) | ||
|
|
||
| # No `min_new_tokens`: the rows have to be free to stop at different steps. | ||
| generate_kwargs = { | ||
| "noise_scheduler": DummyNoiseScheduler(), | ||
| "max_new_tokens": 20, | ||
| "do_sample": False, | ||
| "return_dict_in_generate": True, | ||
| "guidance_scale": 1.3, | ||
| "num_diffusion_steps": 10, | ||
| "use_cache": use_cache, | ||
| } | ||
|
|
||
| # Initialize diffusion with same input for comparable outputs | ||
| def zeros_instead_of_randn(*args, **kwargs): | ||
| return torch.zeros(*args, **kwargs) | ||
|
|
||
| with patch("torch.randn", zeros_instead_of_randn): | ||
| batched = model.generate(input_ids=input_ids, attention_mask=attention_mask, **generate_kwargs) | ||
| per_sample = [ | ||
| model.generate( | ||
| input_ids=input_ids[i : i + 1], | ||
| attention_mask=attention_mask[i : i + 1], | ||
| **generate_kwargs, | ||
| ) | ||
| for i in range(input_ids.shape[0]) | ||
| ] | ||
|
|
||
| for i, single in enumerate(per_sample): | ||
| self.parent.assertEqual( | ||
| batched.audio[i] is None, | ||
| single.audio[0] is None, | ||
| msg=f"Sequence {i}: batched and single-sample generation disagree on whether audio was produced", | ||
| ) | ||
| if batched.audio[i] is not None: | ||
| torch.testing.assert_close( | ||
| batched.audio[i], | ||
| single.audio[0], | ||
| msg=lambda m, i=i: f"Sequence {i} differs between batched and single-sample generation:\n{m}", | ||
| ) | ||
|
|
||
|
|
||
| class VibeVoiceForConditionalGenerationTest(ModelTesterMixin, GenerationTesterMixin, unittest.TestCase): | ||
| all_model_classes = (VibeVoiceForConditionalGeneration,) if is_torch_available() else () | ||
|
|
@@ -302,6 +342,44 @@ def test_vibevoice_generate_max_new_tokens(self): | |
| self.assertIsNotNone(output.audio) | ||
| self.assertEqual(len(output.audio), self.model_tester.batch_size) | ||
|
|
||
| @pytest.mark.generate | ||
| def test_batched_equivalence_with_cache(self): | ||
| """ | ||
| Each decoded audio chunk must be attributed to the sequence that produced it, see | ||
| https://github.com/huggingface/transformers/pull/48902. | ||
| """ | ||
| # Use different input settings to trigger different stopping times for each row, so that a wrong row/audio attribution is observable. | ||
| config_and_inputs = self.model_tester.prepare_config_and_inputs( | ||
| batch_size=4, | ||
| seq_length=4, | ||
| rng=random.Random(7), | ||
| audio_config={ | ||
| **self.model_tester.audio_config, | ||
| "layer_scale_init_value": 0.1, | ||
| "initializer_range": 0.5, | ||
| }, | ||
| ) | ||
| self.model_tester.create_and_check_batched_matches_single(*config_and_inputs, use_cache=True) | ||
|
|
||
| @pytest.mark.generate | ||
| def test_batched_equivalence_without_cache(self): | ||
| """ | ||
| Each decoded audio chunk must be attributed to the sequence that produced it, see | ||
| https://github.com/huggingface/transformers/pull/48902. | ||
| """ | ||
| # Use different input settings to trigger different stopping times for each row, so that a wrong row/audio attribution is observable. | ||
| config_and_inputs = self.model_tester.prepare_config_and_inputs( | ||
| batch_size=4, | ||
| seq_length=4, | ||
| rng=random.Random(7), | ||
| audio_config={ | ||
| **self.model_tester.audio_config, | ||
| "layer_scale_init_value": 0.1, | ||
| "initializer_range": 0.5, | ||
| }, | ||
| ) | ||
| self.model_tester.create_and_check_batched_matches_single(*config_and_inputs, use_cache=False) | ||
|
Comment on lines
+371
to
+381
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. and so that we can do the two line pattern that I think you intended? |
||
|
|
||
| @unittest.skip(reason="Vibevoice has a special cache format so skipping for now") | ||
| def test_cached_decode_matches_cacheless(self): | ||
| pass | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh wow that is easy to miss tho tbf