Fix speaker_encoder silent decay under AdamW weight decay in SFT#351
Open
MR-AI-HuiMin wants to merge 1 commit into
Open
Fix speaker_encoder silent decay under AdamW weight decay in SFT#351MR-AI-HuiMin wants to merge 1 commit into
MR-AI-HuiMin wants to merge 1 commit into
Conversation
speaker_encoder outputs are detached and discarded from CustomVoice exports, but AdamW(model.parameters(), weight_decay=0.01) still shrinks encoder weights. Freeze/exclude the encoder and reuse the locked speaker embedding for training conditioning to match inference.
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
speaker_encoderand exclude it from AdamW infinetuning/sft_12hz.py.codec_embedding(and used by CustomVoice inference).Why this is risky (detailed)
What the current script does
speaker_embedding = model.speaker_encoder(ref_mels).detach().AdamW(model.parameters(), weight_decay=0.01), which includesspeaker_encoder.speaker_encoder.*and writes a fixed vector intotalker.model.codec_embedding.weight[spk_id].codec_embeddingrow — not a livespeaker_encoder.Root cause
.detach()means the TTS loss does not produce useful gradients forspeaker_encoder.AdamW still applies weight decay to those parameters every optimizer step (
θ ← θ - lr * (… + wd * θ)).Net effect:
speaker_encoderis slowly driven toward zero / smaller norms even though it is “not being trained” in the usual sense.Impact on the official single-speaker script
Official code already locks the first embedding for saving, so checkpoint norms do not follow a pure
1/(epoch+1)average collapse.However there is still a train / infer mismatch risk:
codec_embedding.weight_decay) makes this worse.Higher risk for community / multi-speaker forks
Several community trainers (e.g. multi-speaker ports) keep the same
AdamW(model.parameters(), weight_decay=0.01)pattern but average speaker embeddings across the run when saving. In that setting, saved norms empirically collapse roughly as:[
|e_{\text{avg}}| \approx |e_0| / (epoch + 1)
]
when encoder outputs shrink while being accumulated. This directly hurts speaker similarity after SFT.
Related fix already proposed for a multi-speaker trainer: vspeech/Qwen3-TTS-Train#6
Even without averaging, any recipe that:
speaker_encoderin AdamW with nonzeroweight_decay, andis exposing users to silent speaker-identity degradation.
Why freezing is the right fix
speaker_encoder(outputs are detached; weights are discarded from the CustomVoice export anyway).Changes
speaker_encoder.requires_grad_(False)and build AdamW only on remaining trainable params.target_speaker_embeddingfrom the first batch sample; expand/reuse it forinput_codec_embedding[:, 6, :]on all subsequent steps.Test plan
speaker_encoderparameter norms (or a probe forward embedding norm) across epochs — without the patch they trend down under weight decay; with the patch they stay flat.model.safetensorsstill has nospeaker_encoder.*keys and still writescodec_embedding.weight[3000].generate_custom_voiceon the exported checkpoint and check speaker similarity is stable vs an early epoch (no progressive timbre wash-out).Notes / non-goals
speaker_encoderwith real gradients, it should remove.detach(), keep the encoder in the optimizer intentionally, and not drop it from the export — that is a different design.Made with Cursor