Skip to content

Commit f0694ce

Browse files
committed
fix: switch to PyTorch 2.0+ parametrizations weight_norm (matches Applio/VRVC)
- Default to new PyTorch parametrizations weight_norm format (matches Applio and VRVC approach) to fix pretrained model key mismatch - Make convert_old_to_new() and convert_new_to_old() always active (not gated by mode flag) for seamless key conversion at boundaries - Add convert_old_to_new() to all 3 inference loading sites: - arvc/engine/inference/convert.py - arvc/engine/realtime/pipeline.py - arvc/engine/models/onnx/onnx_export.py - Add push_to_hub() function to export models to HuggingFace Hub - Add HuggingFace Hub push section in Export Model tab with 4 inputs: model file, index file, HF token, HF repo - Auto-generate README.md on hub upload - Add huggingface_hub to requirements.txt - Default newpytorch UI checkbox to True
1 parent 3edece3 commit f0694ce

9 files changed

Lines changed: 197 additions & 61 deletions

File tree

arvc/app/tabs/training/child/training.py

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import gradio as gr
44

55

6-
from arvc.services.process import zip_file, fetch_pretrained_data
6+
from arvc.services.process import zip_file, fetch_pretrained_data, push_to_hub
77
from arvc.services.training import preprocess, extract, create_index, training
88
from arvc.utils.variables import translations, model_name, index_path, method_f0, embedders_mode, embedders_model, pretrainedD, pretrainedG, config, file_types, hybrid_f0_method, reference_list
99
from arvc.ui.feedback import gr_warning, visible, unlock_f0, hoplength_show, change_models_choices, get_gpu_info, change_embedders_mode, pitch_guidance_lock, vocoders_lock, unlock_ver, unlock_vocoder, change_pretrained_choices, gpu_number_str, shutil_move, change_reference_choices
@@ -196,8 +196,8 @@ def training_model_tab():
196196
with gr.Group():
197197
newpytorch = gr.Checkbox(
198198
label="New PyTorch 2.0+ Format",
199-
info="Default: OFF = Old format (.weight_g/.weight_v) for broad RVC fork compatibility. Turn ON for PyTorch 2.0+ parametrization format.",
200-
value=False, interactive=True,
199+
info="Default: ON = PyTorch 2.0+ parametrization format (matches Applio/VRVC). Turn OFF only for legacy weight_norm format.",
200+
value=True, interactive=True,
201201
)
202202

203203
# ── Training Options ──
@@ -323,12 +323,44 @@ def on_pretrained_sr_select(model, sr):
323323
zip_model = gr.Button(translations["zip_model"], variant="primary")
324324
zip_output = gr.File(label=translations["output_zip"], file_types=[".zip"], interactive=False, visible=False)
325325

326+
# ── Push to HuggingFace Hub ──
327+
with gr.Accordion("Push to HuggingFace Hub", open=False):
328+
with gr.Row():
329+
hf_model_file = gr.Dropdown(
330+
label="Model file", choices=model_name,
331+
value=model_name[0] if len(model_name) >= 1 else "",
332+
interactive=True, allow_custom_value=True,
333+
)
334+
hf_index_file = gr.Dropdown(
335+
label="Index file", choices=index_path,
336+
value=index_path[0] if len(index_path) >= 1 else "",
337+
interactive=True, allow_custom_value=True,
338+
)
339+
with gr.Row():
340+
hf_token = gr.Textbox(
341+
label="HF Token",
342+
info="HuggingFace API token with write access",
343+
type="password",
344+
interactive=True,
345+
)
346+
hf_repo = gr.Textbox(
347+
label="HF Repo (model)",
348+
info="Target repository (e.g. username/model-name)",
349+
interactive=True,
350+
)
351+
with gr.Row():
352+
hf_refresh = gr.Button(translations["refresh"])
353+
hf_push = gr.Button("Push to Hub", variant="primary")
354+
hf_output = gr.Textbox(label="Status", value="", interactive=False, lines=2)
355+
326356
# ── Event Bindings ──
327357
vocoders.change(fn=pitch_guidance_lock, inputs=[vocoders], outputs=[training_f0])
328358
training_f0.change(fn=vocoders_lock, inputs=[training_f0, vocoders], outputs=[vocoders])
329359
unlock_full_method4.change(fn=unlock_f0, inputs=[unlock_full_method4], outputs=[extract_method])
330360
refresh_file.click(fn=change_models_choices, inputs=[], outputs=[model_file, index_file])
331361
zip_model.click(fn=zip_file, inputs=[training_name, model_file, index_file], outputs=[zip_output])
362+
hf_refresh.click(fn=change_models_choices, inputs=[], outputs=[hf_model_file, hf_index_file])
363+
hf_push.click(fn=push_to_hub, inputs=[hf_model_file, hf_index_file, hf_token, hf_repo], outputs=[hf_output])
332364
dataset_path.change(fn=lambda folder: os.makedirs(folder, exist_ok=True), inputs=[dataset_path], outputs=[])
333365
upload.change(fn=visible, inputs=[upload], outputs=[upload_dataset])
334366
overtraining_detector.change(fn=visible, inputs=[overtraining_detector], outputs=[threshold])

arvc/engine/inference/convert.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def strtobool(val):
3838
from arvc.utils.variables import config, logger, translations
3939
from arvc.engine.inference.audio_processing import preprocess, postprocess
4040
from arvc.engine.models.utils import check_assets, load_audio, load_embedders_model, cut, restore, clear_gpu_cache, load_model
41+
from arvc.engine.models.weight_norm import convert_old_to_new
4142

4243
for l in ["torch", "faiss", "omegaconf", "httpx", "httpcore", "faiss.loader", "numba.core", "urllib3", "transformers", "matplotlib"]:
4344
logging.getLogger(l).setLevel(logging.ERROR)
@@ -439,7 +440,7 @@ def setup(self):
439440
self.net_g = Synthesizer(*self.cpt["config"], use_f0=self.use_f0, text_enc_hidden_dim=768 if self.version == "v2" else 256, vocoder=self.vocoder, checkpointing=self.checkpointing, energy=self.energy)
440441
del self.net_g.enc_q
441442

442-
self.net_g.load_state_dict(self.cpt["weight"], strict=False)
443+
self.net_g.load_state_dict(convert_old_to_new(self.cpt["weight"]), strict=False)
443444
self.net_g.eval().to(self.device)
444445
self.net_g = self.net_g.to(torch.float16 if self.config.is_half else torch.float32)
445446
self.n_spk = self.cpt["config"][-3]

arvc/engine/models/onnx/onnx_export.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from arvc.utils.variables import logger
1212
from arvc.engine.models.algorithms.synthesizers import SynthesizerONNX
13+
from arvc.engine.models.weight_norm import convert_old_to_new
1314

1415
warnings.filterwarnings("ignore")
1516

@@ -26,7 +27,7 @@ def onnx_exporter(input_path, output_path, is_half=False, device="cpu"):
2627
tgt_sr = cpt["config"][-1]
2728

2829
net_g = SynthesizerONNX(*cpt["config"], use_f0=f0, text_enc_hidden_dim=text_enc_hidden_dim, vocoder=vocoder, checkpointing=False, energy=energy_use)
29-
net_g.load_state_dict(cpt["weight"], strict=False)
30+
net_g.load_state_dict(convert_old_to_new(cpt["weight"]), strict=False)
3031
net_g.eval().to(device).to(torch.float16 if is_half else torch.float32)
3132
net_g.remove_weight_norm()
3233

arvc/engine/models/weight_norm.py

100644100755
Lines changed: 38 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,30 @@
11
"""
22
Conditional weight_norm import for RVC fork compatibility.
33
4-
By default, uses the old-style ``torch.nn.utils.weight_norm`` which stores
5-
weights as ``.weight_g`` / ``.weight_v`` — the format used by virtually every
6-
RVC fork. This ensures trained models are interchangeable across projects.
4+
Uses ``torch.nn.utils.parametrizations.weight_norm`` (PyTorch 2.0+) by default,
5+
which stores weights as ``.parametrizations.weight.original0`` / ``original1``.
6+
This matches the approach used by Applio and Vietnamese-RVC.
77
8-
Set the config option ``new_pytorch_weight_norm: true`` to switch to
9-
``torch.nn.utils.parametrizations.weight_norm`` (PyTorch 2.0+), which stores
10-
weights as ``.parametrizations.weight.original0`` / ``original1``.
8+
All checkpoints saved to disk use the old ``.weight_g`` / ``.weight_v`` format
9+
for maximum backward compatibility across RVC forks. Key conversion happens
10+
automatically at save/load boundaries.
1111
"""
1212

1313
import logging
1414

1515
logger = logging.getLogger(__name__)
1616

1717
# ── Global flag ──────────────────────────────────────────────────────────
18-
# False = old-style weight_norm (default, compatible with most RVC forks)
19-
# True = new PyTorch 2.0+ parametrizations.weight_norm
20-
_new_pytorch_mode = False
18+
# True = new PyTorch 2.0+ parametrizations.weight_norm (default, matches Applio/VRVC)
19+
# False = old-style weight_norm (legacy compatibility)
20+
_new_pytorch_mode = True
2121

2222

23-
def configure_weight_norm(new_pytorch: bool = False) -> None:
23+
def configure_weight_norm(new_pytorch: bool = True) -> None:
2424
"""Set the weight_norm mode. Call once at startup before model creation."""
2525
global _new_pytorch_mode
2626
_new_pytorch_mode = bool(new_pytorch)
27-
mode = "new PyTorch 2.0+ parametrizations" if _new_pytorch_mode else "old-style (RVC fork compatible)"
27+
mode = "new PyTorch 2.0+ parametrizations" if _new_pytorch_mode else "old-style (legacy)"
2828
logger.info(f"weight_norm mode: {mode}")
2929

3030

@@ -94,60 +94,45 @@ def weight_norm_g(module):
9494

9595

9696
# ── Key conversion helpers ───────────────────────────────────────────────
97-
# These are only needed when new_pytorch mode is active.
97+
# Always active regardless of mode. This ensures seamless interoperability
98+
# between old-format checkpoints and new-format models (and vice versa).
9899

99-
def needs_key_conversion() -> bool:
100-
"""Return True if checkpoint key conversion between old/new format is needed.
101-
102-
Conversion is needed only when the model uses new PyTorch parametrizations
103-
but checkpoints are stored in old ``.weight_g``/``.weight_v`` format.
104-
"""
105-
return _new_pytorch_mode
100+
def _replace_keys_in_dict(d, old_part, new_part):
101+
"""Recursively replace key substrings in a (possibly nested) dict."""
102+
from collections import OrderedDict
103+
updated = OrderedDict() if isinstance(d, OrderedDict) else {}
104+
for key, value in d.items():
105+
updated[(
106+
key.replace(old_part, new_part) if isinstance(key, str) else key
107+
)] = (
108+
_replace_keys_in_dict(value, old_part, new_part) if isinstance(value, dict) else value
109+
)
110+
return updated
106111

107112

108113
def convert_old_to_new(state_dict):
109114
"""Convert old-style weight_norm keys to new parametrizations keys.
110115
111-
Only applies when ``new_pytorch_weight_norm`` is enabled.
112-
No-op otherwise (model and checkpoint both use old format).
113-
"""
114-
if not _new_pytorch_mode:
115-
return state_dict
116-
117-
from collections import OrderedDict
118-
119-
def _replace(d, old_part, new_part):
120-
updated = OrderedDict() if isinstance(d, OrderedDict) else {}
121-
for key, value in d.items():
122-
updated[key.replace(old_part, new_part) if isinstance(key, str) else key] = (
123-
_replace(value, old_part, new_part) if isinstance(value, dict) else value
124-
)
125-
return updated
116+
Converts ``.weight_v`` → ``.parametrizations.weight.original1``
117+
and ``.weight_g`` → ``.parametrizations.weight.original0``
126118
127-
result = _replace(state_dict, ".weight_v", ".parametrizations.weight.original1")
128-
result = _replace(result, ".weight_g", ".parametrizations.weight.original0")
119+
Always active — ensures old-format checkpoints load correctly into
120+
models that use the new PyTorch parametrization format.
121+
"""
122+
result = _replace_keys_in_dict(state_dict, ".weight_v", ".parametrizations.weight.original1")
123+
result = _replace_keys_in_dict(result, ".weight_g", ".parametrizations.weight.original0")
129124
return result
130125

131126

132127
def convert_new_to_old(state_dict):
133128
"""Convert new parametrizations keys to old-style weight_norm keys.
134129
135-
Only applies when ``new_pytorch_weight_norm`` is enabled.
136-
No-op otherwise (model and checkpoint both use old format).
137-
"""
138-
if not _new_pytorch_mode:
139-
return state_dict
130+
Converts ``.parametrizations.weight.original1`` → ``.weight_v``
131+
and ``.parametrizations.weight.original0`` → ``.weight_g``
140132
141-
from collections import OrderedDict
142-
143-
def _replace(d, old_part, new_part):
144-
updated = OrderedDict() if isinstance(d, OrderedDict) else {}
145-
for key, value in d.items():
146-
updated[key.replace(old_part, new_part) if isinstance(key, str) else key] = (
147-
_replace(value, old_part, new_part) if isinstance(value, dict) else value
148-
)
149-
return updated
150-
151-
result = _replace(state_dict, ".parametrizations.weight.original1", ".weight_v")
152-
result = _replace(result, ".parametrizations.weight.original0", ".weight_g")
133+
Always active — ensures saved checkpoints use the old format for
134+
maximum backward compatibility with other RVC forks.
135+
"""
136+
result = _replace_keys_in_dict(state_dict, ".parametrizations.weight.original1", ".weight_v")
137+
result = _replace_keys_in_dict(result, ".parametrizations.weight.original0", ".weight_g")
153138
return result

arvc/engine/realtime/pipeline.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from arvc.utils.variables import config
1212
from arvc.engine.models.utils import load_embedders_model, extract_features, change_rms, load_faiss_index, load_model
13+
from arvc.engine.models.weight_norm import convert_old_to_new
1314

1415
class Inference:
1516
def get_synthesizer(self, model_path):
@@ -37,7 +38,7 @@ def get_synthesizer(self, model_path):
3738
energy=self.energy
3839
)
3940

40-
net_g.load_state_dict(model["weight"], strict=False)
41+
net_g.load_state_dict(convert_old_to_new(model["weight"]), strict=False)
4142
net_g.eval().to(config.device).to(torch.float16 if config.is_half else torch.float32)
4243
net_g.remove_weight_norm()
4344

arvc/engine/training/runner/train.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ def parse_arguments():
122122
parser.add_argument("--compile_model", type=lambda x: bool(strtobool(x)), default=False, help="Use torch.compile() on generator for PyTorch 2.x speedup")
123123
parser.add_argument("--use_8bit_adam", type=lambda x: bool(strtobool(x)), default=False, help="Use 8-bit Adam optimizer for lower VRAM (requires bitsandbytes)")
124124
parser.add_argument("--grad_accum_steps", type=int, default=1, help="Gradient accumulation steps (reduces VRAM usage with larger effective batch sizes)")
125-
parser.add_argument("--newpytorch", type=lambda x: bool(strtobool(x)), default=False, help="Use PyTorch 2.0+ parametrization format. Default: old format for RVC fork compatibility.")
125+
parser.add_argument("--newpytorch", type=lambda x: bool(strtobool(x)), default=True, help="Use PyTorch 2.0+ parametrization format (default, matches Applio/VRVC). Set false for legacy weight_norm format.")
126126

127127
return parser.parse_args()
128128

arvc/services/process.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,121 @@ def fetch_pretrained_data():
147147
logger.debug(f"Failed to fetch pretrained data: {e}")
148148
return {}
149149

150+
def push_to_hub(model_file, index_file, hf_token, hf_repo):
151+
"""Push a trained RVC model and its index file to HuggingFace Hub.
152+
153+
Args:
154+
model_file: Filename of the .pth model in the weights directory.
155+
index_file: Path to the .index file.
156+
hf_token: HuggingFace API token with write access.
157+
hf_repo: Target HuggingFace repository (e.g. "username/model-name").
158+
159+
Returns:
160+
Status message string.
161+
"""
162+
try:
163+
from huggingface_hub import HfApi, create_repo
164+
except ImportError:
165+
return gr_error("huggingface_hub is not installed. Run: pip install huggingface_hub")
166+
167+
# Validate inputs
168+
if not model_file:
169+
return gr_warning("Please select a model file.")
170+
if not hf_token:
171+
return gr_warning("Please provide a HuggingFace token.")
172+
if not hf_repo:
173+
return gr_warning("Please provide a HuggingFace repository name (e.g. username/model-name).")
174+
175+
# Resolve model path
176+
pth_path = os.path.join(configs["weights_path"], model_file)
177+
if not os.path.exists(pth_path) or not model_file.endswith((".pth", ".onnx")):
178+
return gr_warning(f"Model file not found: {pth_path}")
179+
180+
gr_info(f"Pushing model to HuggingFace Hub: {hf_repo}")
181+
182+
try:
183+
api = HfApi()
184+
185+
# Create repo if it doesn't exist
186+
create_repo(
187+
repo_id=hf_repo,
188+
token=hf_token,
189+
repo_type="model",
190+
exist_ok=True,
191+
private=False,
192+
)
193+
194+
# Upload model file
195+
api.upload_file(
196+
path_or_fileobj=pth_path,
197+
path_in_repo=os.path.basename(pth_path),
198+
repo_id=hf_repo,
199+
token=hf_token,
200+
repo_type="model",
201+
)
202+
203+
# Upload index file if provided
204+
if index_file and os.path.exists(index_file):
205+
api.upload_file(
206+
path_or_fileobj=index_file,
207+
path_in_repo=os.path.basename(index_file),
208+
repo_id=hf_repo,
209+
token=hf_token,
210+
repo_type="model",
211+
)
212+
213+
# Auto-generate README
214+
model_name = os.path.splitext(os.path.basename(model_file))[0]
215+
index_info = f"\n- Index file: `{os.path.basename(index_file)}`" if index_file and os.path.exists(index_file) else ""
216+
readme_content = f"""---
217+
license: mit
218+
tags:
219+
- rvc
220+
- voice-cloning
221+
- advanced-rvc
222+
---
223+
224+
# {model_name}
225+
226+
This model was uploaded to the HuggingFace Hub using [Advanced-RVC-Inference](https://github.com/ArkanDash/Advanced-RVC-Inference).
227+
228+
## Model Files
229+
230+
- Model weights: `{os.path.basename(pth_path)}`{index_info}
231+
232+
## Usage
233+
234+
This model can be used with any RVC-based inference application that supports the RVC v2 format.
235+
236+
## Disclaimer
237+
238+
This model is intended for research and educational purposes only. Please respect the rights of the original voice owners.
239+
"""
240+
241+
# Upload README
242+
import tempfile
243+
with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False, encoding="utf-8") as tmp:
244+
tmp.write(readme_content)
245+
tmp_path = tmp.name
246+
247+
api.upload_file(
248+
path_or_fileobj=tmp_path,
249+
path_in_repo="README.md",
250+
repo_id=hf_repo,
251+
token=hf_token,
252+
repo_type="model",
253+
)
254+
os.unlink(tmp_path)
255+
256+
repo_url = f"https://huggingface.co/{hf_repo}"
257+
gr_info(f"Successfully pushed model to: {repo_url}")
258+
return f"Model pushed to HuggingFace Hub: {repo_url}"
259+
260+
except Exception as e:
261+
logger.error(f"Failed to push model to HuggingFace Hub: {e}")
262+
return gr_error(f"Failed to push to HuggingFace Hub: {e}")
263+
264+
150265
def update_sample_rate_dropdown(model):
151266
data = fetch_pretrained_data()
152267
if not data or model not in data:

arvc/services/training.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ def create_index(model_name, rvc_version, index_algorithm):
144144
for log in log_read(done, "create_index"):
145145
yield log
146146

147-
def training(model_name, rvc_version, save_every_epoch, save_only_latest, save_every_weights, total_epoch, sample_rate, batch_size, gpu, pitch_guidance, not_pretrain, custom_pretrained, pretrain_g, pretrain_d, detector, threshold, clean_up, cache, model_author, vocoder, checkpointing, deterministic, benchmark, optimizer, energy_use, custom_reference=False, reference_name="", multiscale_mel_loss=False, cosine_lr=False, newpytorch=False):
147+
def training(model_name, rvc_version, save_every_epoch, save_only_latest, save_every_weights, total_epoch, sample_rate, batch_size, gpu, pitch_guidance, not_pretrain, custom_pretrained, pretrain_g, pretrain_d, detector, threshold, clean_up, cache, model_author, vocoder, checkpointing, deterministic, benchmark, optimizer, energy_use, custom_reference=False, reference_name="", multiscale_mel_loss=False, cosine_lr=False, newpytorch=True):
148148
sr = int(float(sample_rate.rstrip("k")) * 1000)
149149
if not model_name: return gr_warning(translations["provide_name"])
150150

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ edge-tts>=7.2.0
5050
ffmpy==0.3.1
5151
ffmpeg-python>=0.2.0
5252
beautifulsoup4
53+
huggingface_hub
5354

5455
# Tensorboard and ONNX
5556
tensorboard

0 commit comments

Comments
 (0)