525 lines
21 KiB
Python
525 lines
21 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
export_cond_enc.py — Export AceStep Condition Encoder to ONNX.
|
|
|
|
The condition encoder takes outputs from the text encoder (text_hidden + lyric_embed)
|
|
and reference audio features (timbre_feats), and produces enc_hidden for DiT cross-attention.
|
|
|
|
Internal architecture:
|
|
- text_projector: Linear(1024→2048, no bias) — projects text encoder output
|
|
- lyric_encoder: Linear(1024→2048)+bias → 8-layer bidirectional Qwen3 → RMSNorm
|
|
- timbre_encoder: Linear(64→2048)+bias → [CLS prepend] → 4-layer bidir Qwen3 → RMSNorm → position[0]
|
|
- cat(lyric_out, timbre_out, text_proj_out) → enc_hidden [B, S_total, 2048]
|
|
|
|
Usage:
|
|
python export_cond_enc.py --model-dir <path-to-DiT-safetensors> --output <output.onnx>
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import time
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
import numpy as np
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
|
|
class CondEncoderWrapper(nn.Module):
|
|
"""Wrapper for ONNX export that simplifies the condition encoder interface.
|
|
|
|
For inference (batch_size=1), we simplify:
|
|
- No pack_sequences sorting (all tokens are valid, no padding)
|
|
- Timbre: single reference, so unpack is trivial (just unsqueeze)
|
|
- Output is simple cat(lyric, timbre, text_proj)
|
|
|
|
ONNX inputs:
|
|
text_hidden: [B, S_text, 1024] fp16 — from text encoder
|
|
lyric_embed: [B, S_lyric, 1024] fp16 — from embedding table lookup
|
|
timbre_feats: [B, S_ref, 64] fp16 — from VAE encoder (or zeros)
|
|
has_timbre: [1] int64 — 1 if timbre is present, 0 if not
|
|
|
|
ONNX output:
|
|
enc_hidden: [B, S_total, 2048] fp16 — packed conditioning
|
|
"""
|
|
|
|
def __init__(self, cond_encoder):
|
|
super().__init__()
|
|
self.text_projector = cond_encoder.text_projector
|
|
self.lyric_encoder = cond_encoder.lyric_encoder
|
|
self.timbre_encoder = cond_encoder.timbre_encoder
|
|
|
|
def forward(self, text_hidden, lyric_embed, timbre_feats, has_timbre):
|
|
"""
|
|
Forward pass with simplified interface for ONNX export.
|
|
|
|
Note: For ONNX tracing, has_timbre must be a tensor, not a Python bool.
|
|
We use torch.where / masking to handle the conditional timbre path.
|
|
"""
|
|
B = text_hidden.shape[0]
|
|
|
|
# 1) Text projection: [B, S_text, 1024] → [B, S_text, 2048]
|
|
text_proj = self.text_projector(text_hidden)
|
|
|
|
# 2) Lyric encoding: [B, S_lyric, 1024] → 8L bidir Qwen3 → [B, S_lyric, 2048]
|
|
S_lyric = lyric_embed.shape[1]
|
|
lyric_mask = torch.ones(B, S_lyric, device=lyric_embed.device, dtype=torch.long)
|
|
lyric_out = self.lyric_encoder(
|
|
inputs_embeds=lyric_embed,
|
|
attention_mask=lyric_mask,
|
|
)
|
|
if hasattr(lyric_out, 'last_hidden_state'):
|
|
lyric_out = lyric_out.last_hidden_state
|
|
else:
|
|
lyric_out = lyric_out[0]
|
|
|
|
# 3) Timbre encoding: [B, S_ref, 64] → 4L bidir Qwen3 → position[0] → [B, 1, 2048]
|
|
# For ONNX: we always run the timbre path but zero out if has_timbre=0
|
|
S_ref = timbre_feats.shape[1]
|
|
timbre_mask = torch.ones(1, S_ref, device=timbre_feats.device, dtype=torch.long)
|
|
# refer_audio_order_mask: all 0s means everything belongs to batch 0
|
|
order_mask = torch.zeros(1, device=timbre_feats.device, dtype=torch.long)
|
|
|
|
# Reshape for timbre encoder: expects [N_packed, S_ref, 64]
|
|
timbre_input = timbre_feats # [B, S_ref, 64]
|
|
timbre_embs, timbre_embs_mask = self.timbre_encoder(
|
|
refer_audio_acoustic_hidden_states_packed=timbre_input,
|
|
refer_audio_order_mask=order_mask,
|
|
attention_mask=timbre_mask,
|
|
)
|
|
# timbre_embs: [B, 1, 2048] — CLS token output per batch
|
|
|
|
# 4) Concatenate: [lyric, timbre, text_proj]
|
|
# When has_timbre=0, skip timbre in the concatenation
|
|
# For ONNX compatibility, always cat but mask the timbre contribution
|
|
ht = has_timbre[0]
|
|
if ht > 0:
|
|
enc_hidden = torch.cat([lyric_out, timbre_embs, text_proj], dim=1)
|
|
else:
|
|
enc_hidden = torch.cat([lyric_out, text_proj], dim=1)
|
|
|
|
return enc_hidden
|
|
|
|
|
|
class CondEncoderWrapperFixed(nn.Module):
|
|
"""Fixed version that always includes timbre (simplifies ONNX graph).
|
|
|
|
For inference, timbre is always present (silence latent as zero timbre).
|
|
This avoids dynamic control flow in the ONNX graph.
|
|
|
|
IMPORTANT: The timbre encoder's forward() uses unpack_timbre_embeddings()
|
|
which has data-dependent control flow (refer_audio_order_mask.max().item()).
|
|
torch.export cannot handle this. So we manually invoke the timbre encoder's
|
|
sub-components: embed_tokens → CLS prepend → transformer layers → norm →
|
|
take position 0. This is equivalent for B=1 inference.
|
|
|
|
ONNX inputs:
|
|
text_hidden: [B, S_text, 1024] fp16
|
|
lyric_embed: [B, S_lyric, 1024] fp16
|
|
timbre_feats: [B, S_ref, 64] fp16 (zeros if no reference)
|
|
|
|
ONNX output:
|
|
enc_hidden: [B, S_total, 2048] fp16 where S_total = S_lyric + 1 + S_text
|
|
"""
|
|
|
|
def __init__(self, cond_encoder):
|
|
super().__init__()
|
|
self.text_projector = cond_encoder.text_projector
|
|
self.lyric_encoder = cond_encoder.lyric_encoder
|
|
# Extract timbre encoder sub-components for manual invocation
|
|
self.timbre_embed_tokens = cond_encoder.timbre_encoder.embed_tokens
|
|
self.timbre_special_token = cond_encoder.timbre_encoder.special_token
|
|
self.timbre_norm = cond_encoder.timbre_encoder.norm
|
|
self.timbre_rotary_emb = cond_encoder.timbre_encoder.rotary_emb
|
|
self.timbre_layers = cond_encoder.timbre_encoder.layers
|
|
self.timbre_config = cond_encoder.timbre_encoder.config
|
|
|
|
def _timbre_forward_simple(self, timbre_feats):
|
|
"""Run the timbre encoder without unpack_timbre_embeddings.
|
|
|
|
timbre_feats: [B, S_ref, 64]
|
|
Returns: [B, 1, hidden_size] — CLS token output
|
|
"""
|
|
B = timbre_feats.shape[0]
|
|
|
|
# Project: [B, S_ref, 64] → [B, S_ref, hidden_size]
|
|
inputs_embeds = self.timbre_embed_tokens(timbre_feats)
|
|
|
|
# Prepend CLS token: [B, S_ref+1, hidden_size]
|
|
cls_token = self.timbre_special_token.expand(B, 1, -1)
|
|
inputs_embeds = torch.cat([cls_token, inputs_embeds], dim=1)
|
|
|
|
S = inputs_embeds.shape[1]
|
|
|
|
# Position IDs and RoPE
|
|
cache_position = torch.arange(0, S, device=inputs_embeds.device)
|
|
position_ids = cache_position.unsqueeze(0)
|
|
position_embeddings = self.timbre_rotary_emb(inputs_embeds, position_ids)
|
|
|
|
# Build attention mask (full bidirectional, no padding)
|
|
# Using None for SDPA = no mask = full attention
|
|
hidden_states = inputs_embeds
|
|
|
|
for layer in self.timbre_layers:
|
|
layer_outputs = layer(
|
|
hidden_states,
|
|
position_embeddings,
|
|
None, # attention_mask=None → full bidirectional
|
|
position_ids,
|
|
)
|
|
hidden_states = layer_outputs[0]
|
|
|
|
hidden_states = self.timbre_norm(hidden_states)
|
|
|
|
# Extract CLS token (position 0): [B, hidden_size]
|
|
timbre_emb = hidden_states[:, 0:1, :] # [B, 1, hidden_size]
|
|
|
|
return timbre_emb
|
|
|
|
def forward(self, text_hidden, lyric_embed, timbre_feats):
|
|
B = text_hidden.shape[0]
|
|
|
|
# 1) Text projection
|
|
text_proj = self.text_projector(text_hidden)
|
|
|
|
# 2) Lyric encoding
|
|
S_lyric = lyric_embed.shape[1]
|
|
lyric_mask = torch.ones(B, S_lyric, device=lyric_embed.device, dtype=torch.long)
|
|
lyric_out = self.lyric_encoder(
|
|
inputs_embeds=lyric_embed,
|
|
attention_mask=lyric_mask,
|
|
)
|
|
if hasattr(lyric_out, 'last_hidden_state'):
|
|
lyric_out = lyric_out.last_hidden_state
|
|
else:
|
|
lyric_out = lyric_out[0]
|
|
|
|
# 3) Timbre encoding — manual path (bypasses unpack_timbre_embeddings)
|
|
timbre_embs = self._timbre_forward_simple(timbre_feats)
|
|
# timbre_embs: [B, 1, 2048]
|
|
|
|
# 4) Concatenate: lyric + timbre + text_proj
|
|
enc_hidden = torch.cat([lyric_out, timbre_embs, text_proj], dim=1)
|
|
|
|
return enc_hidden
|
|
|
|
|
|
def load_model(model_dir: str, device: str = "cuda", dtype=torch.float32):
|
|
"""Load the AceStep model and extract the condition encoder."""
|
|
model_dir = Path(model_dir)
|
|
|
|
if sys.platform == "win32":
|
|
import io
|
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
|
|
|
# Monkey-patch transformers auto_docstring
|
|
try:
|
|
import transformers.utils.auto_docstring as _ad
|
|
_ad.auto_docstring = lambda *a, **kw: (lambda cls: cls)
|
|
except Exception:
|
|
pass
|
|
|
|
sys.path.insert(0, str(model_dir))
|
|
|
|
# Create a stub AceStepConfig module to bypass the 'acestep' package import.
|
|
# The real AceStepConfig is a PretrainedConfig subclass. We construct it
|
|
# using AutoConfig which reads config.json and finds the auto_map.
|
|
# But first we need the config module to exist so the model code can import it.
|
|
import json
|
|
import types
|
|
from transformers import PretrainedConfig
|
|
|
|
with open(model_dir / "config.json") as f:
|
|
config_dict = json.load(f)
|
|
|
|
# Create AceStepConfig class dynamically from config.json
|
|
class AceStepConfig(PretrainedConfig):
|
|
model_type = "acestep"
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
# Set all config keys as attributes
|
|
for k, v in kwargs.items():
|
|
if not hasattr(self, k):
|
|
setattr(self, k, v)
|
|
# Ensure critical attrs have defaults
|
|
if not hasattr(self, 'text_hidden_dim'):
|
|
self.text_hidden_dim = 1024
|
|
if not hasattr(self, 'timbre_hidden_dim'):
|
|
self.timbre_hidden_dim = 64
|
|
if not hasattr(self, 'encoder_hidden_size'):
|
|
self.encoder_hidden_size = 2048
|
|
if not hasattr(self, 'encoder_intermediate_size'):
|
|
self.encoder_intermediate_size = 6144
|
|
if not hasattr(self, 'encoder_num_attention_heads'):
|
|
self.encoder_num_attention_heads = 16
|
|
if not hasattr(self, 'encoder_num_key_value_heads'):
|
|
self.encoder_num_key_value_heads = 8
|
|
if not hasattr(self, 'num_lyric_encoder_hidden_layers'):
|
|
self.num_lyric_encoder_hidden_layers = 8
|
|
if not hasattr(self, 'num_timbre_encoder_hidden_layers'):
|
|
self.num_timbre_encoder_hidden_layers = 4
|
|
if not hasattr(self, 'num_attention_pooler_hidden_layers'):
|
|
self.num_attention_pooler_hidden_layers = 2
|
|
if not hasattr(self, 'out_channels'):
|
|
self.out_channels = 64
|
|
if not hasattr(self, 'in_channels'):
|
|
self.in_channels = 192
|
|
|
|
# Register the stub config module
|
|
stub_mod = types.ModuleType("configuration_acestep_v15")
|
|
stub_mod.AceStepConfig = AceStepConfig
|
|
sys.modules["configuration_acestep_v15"] = stub_mod
|
|
|
|
# Create stub acestep package hierarchy. Each intermediate module needs
|
|
# __path__ set so it acts as a package (allows submodule imports).
|
|
for mod_name in ["acestep", "acestep.models", "acestep.models.common"]:
|
|
m = types.ModuleType(mod_name)
|
|
m.__path__ = [] # makes it act as a package
|
|
sys.modules[mod_name] = m
|
|
|
|
# Register configuration_acestep_v15 under the acestep.models.common path
|
|
cfg_mod = types.ModuleType("acestep.models.common.configuration_acestep_v15")
|
|
cfg_mod.AceStepConfig = AceStepConfig
|
|
sys.modules["acestep.models.common.configuration_acestep_v15"] = cfg_mod
|
|
|
|
# Create apg_guidance stub — these functions are used by the DiT diffusion
|
|
# loop but NOT by the condition encoder. Provide dummies to satisfy import.
|
|
class MomentumBuffer:
|
|
def __init__(self, *a, **kw): pass
|
|
def _apg_stub(*a, **kw): return None
|
|
|
|
apg_stub = types.ModuleType("acestep.models.common.apg_guidance")
|
|
apg_stub.MomentumBuffer = MomentumBuffer
|
|
apg_stub.adg_forward = _apg_stub
|
|
apg_stub.adg_w_norm_forward = _apg_stub
|
|
apg_stub.adg_wo_clip_forward = _apg_stub
|
|
apg_stub.apg_forward = _apg_stub
|
|
apg_stub.cfg_forward = _apg_stub
|
|
apg_stub.call_cos_tensor = _apg_stub
|
|
apg_stub.compute_perpendicular_component = _apg_stub
|
|
apg_stub.project = _apg_stub
|
|
sys.modules["acestep.models.common.apg_guidance"] = apg_stub
|
|
|
|
# Also register the local apg_guidance module
|
|
apg_local = types.ModuleType("apg_guidance")
|
|
apg_local.MomentumBuffer = MomentumBuffer
|
|
apg_local.adg_forward = _apg_stub
|
|
apg_local.apg_forward = _apg_stub
|
|
apg_local.cfg_forward = _apg_stub
|
|
sys.modules["apg_guidance"] = apg_local
|
|
|
|
config = AceStepConfig(**config_dict)
|
|
config._attn_implementation = "sdpa"
|
|
|
|
# The encoder's Qwen3 sub-models (lyric/timbre) use encoder_hidden_size
|
|
# as their hidden_size. Set it on the config so Qwen3RotaryEmbedding works.
|
|
# transformers 5.x requires rope_parameters dict.
|
|
if not hasattr(config, 'rope_parameters') or config.rope_parameters is None:
|
|
config.rope_parameters = {
|
|
"rope_type": "default",
|
|
"rope_theta": config.rope_theta if hasattr(config, 'rope_theta') else 1000000.0,
|
|
}
|
|
|
|
print(f"[export_cond_enc] Loading model from {model_dir}...")
|
|
t0 = time.time()
|
|
|
|
# The full model creates a separate encoder config with encoder-specific
|
|
# dimensions (see AceStepConditionGenerationModel.__init__ lines 1621-1628).
|
|
# The encoder uses encoder_hidden_size (2048), not the DiT hidden_size (2560).
|
|
import copy
|
|
encoder_config = copy.deepcopy(config)
|
|
encoder_config.hidden_size = config.encoder_hidden_size
|
|
encoder_config.intermediate_size = config.encoder_intermediate_size
|
|
encoder_config.num_attention_heads = config.encoder_num_attention_heads
|
|
encoder_config.num_key_value_heads = config.encoder_num_key_value_heads
|
|
|
|
from modeling_acestep_v15_xl_base import AceStepConditionEncoder
|
|
|
|
cond_encoder = AceStepConditionEncoder(encoder_config)
|
|
|
|
# Load weights — filter to encoder.* prefix
|
|
from safetensors.torch import load_file
|
|
st_path = model_dir / "model.safetensors"
|
|
state_dict = load_file(str(st_path))
|
|
|
|
cond_state_dict = {}
|
|
for k, v in state_dict.items():
|
|
if k.startswith("encoder."):
|
|
cond_state_dict[k[len("encoder."):]] = v
|
|
|
|
missing, unexpected = cond_encoder.load_state_dict(cond_state_dict, strict=False)
|
|
if missing:
|
|
print(f"[export_cond_enc] Warning: {len(missing)} missing keys (first 5: {missing[:5]})")
|
|
if unexpected:
|
|
print(f"[export_cond_enc] Warning: {len(unexpected)} unexpected keys (first 5: {unexpected[:5]})")
|
|
|
|
cond_encoder = cond_encoder.to(device=device, dtype=dtype)
|
|
cond_encoder.eval()
|
|
|
|
t1 = time.time()
|
|
n_params = sum(p.numel() for p in cond_encoder.parameters()) / 1e6
|
|
print(f"[export_cond_enc] Model loaded in {t1-t0:.1f}s ({n_params:.0f}M params)")
|
|
print(f"[export_cond_enc] text_hidden_dim={encoder_config.text_hidden_dim}, hidden_size={encoder_config.hidden_size}")
|
|
|
|
return cond_encoder, encoder_config
|
|
|
|
|
|
def export_onnx(cond_encoder, config, output_path: str, opset: int = 18):
|
|
"""Export the condition encoder to ONNX."""
|
|
device = next(cond_encoder.parameters()).device
|
|
dtype = next(cond_encoder.parameters()).dtype
|
|
|
|
wrapper = CondEncoderWrapperFixed(cond_encoder)
|
|
wrapper.eval()
|
|
|
|
# Dummy inputs
|
|
B = 1
|
|
S_text = 64
|
|
S_lyric = 128
|
|
S_ref = 8 # 8 frames of reference audio (short clip)
|
|
|
|
dummy_text_hidden = torch.randn(B, S_text, config.text_hidden_dim, device=device, dtype=dtype)
|
|
dummy_lyric_embed = torch.randn(B, S_lyric, config.text_hidden_dim, device=device, dtype=dtype)
|
|
dummy_timbre_feats = torch.randn(B, S_ref, config.timbre_hidden_dim, device=device, dtype=dtype)
|
|
|
|
print(f"[export_cond_enc] Tracing with shapes: text={list(dummy_text_hidden.shape)}, "
|
|
f"lyric={list(dummy_lyric_embed.shape)}, timbre={list(dummy_timbre_feats.shape)}")
|
|
|
|
# Test forward
|
|
print("[export_cond_enc] Testing forward pass...")
|
|
with torch.no_grad():
|
|
test_out = wrapper(dummy_text_hidden, dummy_lyric_embed, dummy_timbre_feats)
|
|
expected_S = S_lyric + 1 + S_text # lyric + timbre(1) + text
|
|
print(f"[export_cond_enc] Output shape: {list(test_out.shape)} "
|
|
f"(expected [{B}, {expected_S}, {config.hidden_size}])")
|
|
|
|
# Export
|
|
print(f"[export_cond_enc] Exporting to ONNX (opset {opset})...")
|
|
t0 = time.time()
|
|
|
|
torch.onnx.export(
|
|
wrapper,
|
|
(dummy_text_hidden, dummy_lyric_embed, dummy_timbre_feats),
|
|
output_path,
|
|
opset_version=opset,
|
|
input_names=["text_hidden", "lyric_embed", "timbre_feats"],
|
|
output_names=["enc_hidden"],
|
|
dynamic_axes={
|
|
"text_hidden": {0: "batch", 1: "text_seq"},
|
|
"lyric_embed": {0: "batch", 1: "lyric_seq"},
|
|
"timbre_feats": {0: "batch", 1: "timbre_seq"},
|
|
"enc_hidden": {0: "batch", 1: "enc_seq"},
|
|
},
|
|
do_constant_folding=True,
|
|
export_params=True,
|
|
)
|
|
|
|
t1 = time.time()
|
|
file_size = os.path.getsize(output_path)
|
|
print(f"[export_cond_enc] Exported to {output_path}")
|
|
print(f"[export_cond_enc] File size: {file_size/1e6:.1f} MB")
|
|
print(f"[export_cond_enc] Export time: {t1-t0:.1f}s")
|
|
|
|
|
|
def export_null_cond_emb(model_dir: str, output_path: str):
|
|
"""Export null_condition_emb as raw float32 binary."""
|
|
from safetensors.torch import load_file
|
|
model_dir = Path(model_dir)
|
|
st_path = model_dir / "model.safetensors"
|
|
|
|
state_dict = load_file(str(st_path))
|
|
key = "null_condition_emb"
|
|
if key not in state_dict:
|
|
print(f"[export_cond_enc] WARNING: {key} not found, skipping")
|
|
return
|
|
|
|
vec = state_dict[key].detach().cpu().float().numpy().flatten()
|
|
with open(output_path, "wb") as f:
|
|
f.write(struct.pack("<I", len(vec)))
|
|
f.write(vec.tobytes())
|
|
|
|
print(f"[export_cond_enc] null_condition_emb: [{len(vec)}] -> {output_path} ({len(vec)*4} bytes)")
|
|
|
|
|
|
def verify_onnx(onnx_path: str, cond_encoder, config):
|
|
"""Verify ONNX output matches PyTorch."""
|
|
try:
|
|
import onnxruntime as ort
|
|
except ImportError:
|
|
print("[export_cond_enc] onnxruntime not installed, skipping verification")
|
|
return
|
|
|
|
device = next(cond_encoder.parameters()).device
|
|
dtype = next(cond_encoder.parameters()).dtype
|
|
wrapper = CondEncoderWrapperFixed(cond_encoder)
|
|
wrapper.eval()
|
|
|
|
B, S_text, S_lyric, S_ref = 1, 32, 64, 8
|
|
text_hidden = torch.randn(B, S_text, config.text_hidden_dim, device=device, dtype=dtype)
|
|
lyric_embed = torch.randn(B, S_lyric, config.text_hidden_dim, device=device, dtype=dtype)
|
|
timbre_feats = torch.randn(B, S_ref, config.timbre_hidden_dim, device=device, dtype=dtype)
|
|
|
|
with torch.no_grad():
|
|
ref_out = wrapper(text_hidden, lyric_embed, timbre_feats).cpu().float().numpy()
|
|
|
|
providers = ["CUDAExecutionProvider", "CPUExecutionProvider"]
|
|
sess = ort.InferenceSession(onnx_path, providers=providers)
|
|
ort_out = sess.run(None, {
|
|
"text_hidden": text_hidden.cpu().float().numpy(),
|
|
"lyric_embed": lyric_embed.cpu().float().numpy(),
|
|
"timbre_feats": timbre_feats.cpu().float().numpy(),
|
|
})[0]
|
|
|
|
max_diff = np.max(np.abs(ref_out - ort_out))
|
|
mean_diff = np.mean(np.abs(ref_out - ort_out))
|
|
print(f"[export_cond_enc] Verification: max_diff={max_diff:.6f}, mean_diff={mean_diff:.6f}")
|
|
|
|
if max_diff < 0.05:
|
|
print("[export_cond_enc] PASS: ONNX output matches PyTorch")
|
|
else:
|
|
print("[export_cond_enc] WARNING: Large difference — may need investigation")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Export AceStep condition encoder to ONNX")
|
|
parser.add_argument("--model-dir", required=True,
|
|
help="Path to the DiT model directory (contains encoder weights)")
|
|
parser.add_argument("--output", default=None,
|
|
help="Output ONNX file (default: models/onnx/cond_encoder.onnx)")
|
|
parser.add_argument("--opset", type=int, default=18)
|
|
parser.add_argument("--verify", action="store_true")
|
|
parser.add_argument("--device", default="cuda")
|
|
args = parser.parse_args()
|
|
|
|
if args.output is None:
|
|
onnx_dir = Path(args.model_dir).parent / "onnx"
|
|
onnx_dir.mkdir(parents=True, exist_ok=True)
|
|
args.output = str(onnx_dir / "cond_encoder.onnx")
|
|
|
|
os.makedirs(os.path.dirname(args.output), exist_ok=True)
|
|
output_dir = os.path.dirname(args.output)
|
|
|
|
# Load model
|
|
cond_encoder, config = load_model(args.model_dir, device=args.device)
|
|
|
|
# Export ONNX
|
|
export_onnx(cond_encoder, config, args.output, opset=args.opset)
|
|
|
|
# Export null_condition_emb
|
|
null_cond_path = os.path.join(output_dir, "null_condition_emb.bin")
|
|
export_null_cond_emb(args.model_dir, null_cond_path)
|
|
|
|
# Verify
|
|
if args.verify:
|
|
verify_onnx(args.output, cond_encoder, config)
|
|
|
|
print("[export_cond_enc] Done!")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|