File size: 5,152 Bytes
10bebcd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# Integrating SR-FD into a few-step flow-matching TTS model

SR-FD is model-agnostic: it only needs (1) a **differentiable** few-step
sampler and (2) the generated waveform. This repository implements the loss and
extractors; the base model is the external, tokenizer-free flow-matching TTS
model VoxCPM2 (`openbmb/VoxCPM2`), used through a LoRA adapter. This document
shows the three integration points so the method can be reproduced or ported.

## 1. A differentiable few-step sampler

Many flow-matching decoders wrap their sampling loop in
`@torch.inference_mode()`, which blocks gradients. SR-FD needs gradients to
flow from the loss, through the generated latent, back into the (LoRA) weights.
Add a sibling `sample()` method with identical numerics but **without** the
inference-mode decorator. In VoxCPM2 this lives on the flow-matching decoder
(`UnifiedCFM`):

```python
def sample(self, mu, n_timesteps, patch_size, cond,
           temperature=1.0, cfg_value=1.0, sway_sampling_coef=1.0,
           use_cfg_zero_star=True, initial_noise=None, return_trajectory=False):
    """Differentiable sampling path used by SR-FD.

    Identical in numerics to `forward` but without `inference_mode`, so
    gradients can flow back through the produced latent into `mu`.
    """
    # ... same Euler/Heun/RK integration as the deployment sampler ...
```

The model's `forward` then exposes a `sample_with_grad` flag that routes
generation through `sample()` instead of the inference-mode path, using the
**same** step count and sampler settings as deployment (four Euler steps,
guidance 2.45, sway 1.0). This is what makes the loss act on the distribution
the sampler will actually produce, not on a teacher-forced trajectory.

## 2. Building the loss

Build the extractors and the loss once, from the `srfd` block of the config:

```python
import yaml, torch
from srfd import SRFDEmaLoss, build_srfd_extractors, load_stats

cfg = yaml.safe_load(open("configs/srfd_compact3.yaml"))["srfd"]

extractors = build_srfd_extractors(cfg["reps"])           # Whisper + CTC
targets = [                                               # three reference targets
    {"name": t["name"], "weight": t["weight"], "stats": load_stats(t["path"])}
    for t in cfg["reference_stats_paths"]
]
srfd_loss = SRFDEmaLoss(
    extractors=extractors,
    real_stats=targets,
    stats_mode=cfg["stats_mode"],          # "queue"
    queue_size=cfg["queue_size"],          # 50000
    normalize=cfg["normalize"],            # per-term FD normalization
    normalize_total_weight=cfg["normalize_total_weight"],
    warmup_steps=cfg["warmup_steps"],
)
```

## 3. The training step

On each step: (a) sample a short utterance with the differentiable four-step
sampler, (b) decode it to a waveform, (c) apply the length gate, (d) call the
SR-FD loss, and (e) add it to the base objective. Sketch:

```python
# (a) differentiable few-step generation (same settings as deployment)
gen_latent = model(batch, sample_with_grad=True, sample_n_timesteps=4)
# (b) decode to waveform via the (frozen) AudioVAE decoder
wav = model.audio_vae.decode(gen_latent)

# (c) length gate: only keep samples whose duration ratio is in [0.92, 1.08]
ratio = generated_duration / target_duration
keep = (ratio >= 0.92) & (ratio <= 1.08)

# (d) SR-FD reads the generated waveform (+ mask + sample rate)
srfd_batch = {
    "waveform": wav[keep],
    "waveform_mask": wav_mask[keep],
    "waveform_sample_rate": out_sample_rate,
}
out = srfd_loss(srfd_batch, step=global_step)   # {"loss/srfd": ...}

# (e) total objective
loss = (w_fm   * out_fm["loss/diff"]
        + w_stop * out_stop["loss/stop"]
        + L_aux
        + lambda_srfd * out["loss/srfd"])       # lambda_srfd = 2e-4
loss.backward()
```

### Numerical notes

* The Fréchet term uses `torch.linalg.eigh`, which has no bf16 CUDA kernel.
  Wrap the SR-FD call in `torch.amp.autocast(device_type="cuda", enabled=False)`
  so the eigendecomposition runs in fp32 while the rest of the step stays bf16.
* The queue detaches features from previous steps, so the autograd graph never
  grows across steps; only the current mini-batch carries gradient.
* SR-FD activates after `warmup_steps`, so the base losses stabilize training
  before the distributional term turns on.

## 4. Inference (deployment)

At test time SR-FD is gone entirely — the deployed model is the base four-step
model plus the LoRA adapter. Loading the adapter and generating:

With a current upstream `voxcpm` installation, load the adapter when the base
model is constructed and use the public `inference_timesteps` argument:

```python
import soundfile as sf
from voxcpm import VoxCPM

model = VoxCPM.from_pretrained(
    "openbmb/VoxCPM2",
    load_denoiser=False,
    lora_weights_path="demo/model",
)
wav = model.generate(
    text="The quick brown fox jumps over the lazy dog.",
    inference_timesteps=4,
    cfg_value=2.35,
    normalize=True,
    denoise=False,
    seed=0,
)
sf.write("srfd.wav", wav, model.tts_model.sample_rate)
```

No extractors, queues, reference moments, or Fréchet computation are involved at
inference, so there is no added inference cost.