| """Project-local inference memory optimizations for the S2-Pro DAC codec. |
| |
| The pinned Fish Speech source remains unchanged. This module builds the same |
| codec and loads the same checkpoint, then removes buffers that are unnecessary |
| for the window-limited inference path before the model is moved to CUDA. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import gc |
| import io |
| import math |
| import os |
| import threading |
| import time |
| from pathlib import Path |
| from typing import Any |
|
|
| import torch |
|
|
| os.environ.setdefault("TEMPORARILY_DISABLE_PROTOBUF_VERSION_CHECK", "true") |
|
|
| from fish_speech.models.dac.modded_dac import DAC |
|
|
|
|
| def load_reference_audio_soundfile( |
| reference_audio: bytes | str | Path, |
| sample_rate: int, |
| ): |
| """Decode API reference audio without TorchCodec. |
| |
| Torch 2.11 routes ``torchaudio.load`` through optional TorchCodec. The |
| pinned container already includes SoundFile, which supports the WAV/FLAC |
| inputs accepted by this service, so no environment package mutation is |
| needed. |
| """ |
|
|
| import numpy as np |
| import soundfile as sf |
| import torchaudio |
|
|
| source = ( |
| io.BytesIO(reference_audio) |
| if isinstance(reference_audio, bytes) |
| else reference_audio |
| ) |
| audio, original_rate = sf.read(source, dtype="float32", always_2d=True) |
| mono = np.asarray(audio.mean(axis=1), dtype=np.float32) |
| if original_rate != sample_rate: |
| mono = ( |
| torchaudio.functional.resample( |
| torch.from_numpy(mono), |
| original_rate, |
| sample_rate, |
| ) |
| .contiguous() |
| .numpy() |
| ) |
| return mono |
|
|
|
|
| @torch.inference_mode() |
| def warm_reference_encoder( |
| codec: torch.nn.Module, |
| device: str | torch.device, |
| seconds: float = 1.0, |
| ) -> dict[str, Any]: |
| """Prime lazy codec state before the first user reference is cached. |
| |
| The staged BF16 encoder produces a different discrete code sequence on its |
| first CUDA pass. A discarded silence pass makes subsequent encodes bit |
| stable, preventing the first uploaded voice from being conditioned on an |
| avoidable cold-start code path. |
| """ |
|
|
| if seconds <= 0: |
| raise ValueError("Reference warmup duration must be positive") |
| target = torch.device(device) |
| sample_rate = int(codec.sample_rate) |
| samples = int(round(sample_rate * seconds)) |
| audio = torch.zeros((1, samples), dtype=torch.float32) |
| lengths = torch.tensor([samples], device=target, dtype=torch.long) |
| started = time.perf_counter() |
| codes = codec.encode(audio, lengths)[0][0].cpu() |
| return { |
| "input": "digital_silence", |
| "seconds": seconds, |
| "samples": samples, |
| "code_frames": int(codes.shape[-1]), |
| "elapsed_seconds": time.perf_counter() - started, |
| } |
|
|
|
|
| class StagedReferenceCodec(DAC): |
| """DAC with decode modules resident and reference-only modules staged.""" |
|
|
| @property |
| def device(self) -> torch.device: |
| return self._decode_device |
|
|
| def configure_reference_staging( |
| self, |
| decode_device: str | torch.device, |
| offload_device: str | torch.device = "cpu", |
| ) -> None: |
| self._decode_device = torch.device(decode_device) |
| self._reference_offload_device = torch.device(offload_device) |
| self._reference_lock = threading.Lock() |
|
|
| |
| self.quantizer.semantic_quantizer.to(self._decode_device) |
| self.quantizer.quantizer.to(self._decode_device) |
| self.quantizer.post_module.to(self._decode_device) |
| self.quantizer.upsample.to(self._decode_device) |
| self.decoder.to(self._decode_device) |
|
|
| |
| self.encoder.to(self._reference_offload_device) |
| self.quantizer.downsample.to(self._reference_offload_device) |
| self.quantizer.pre_module.to(self._reference_offload_device) |
|
|
| @torch.inference_mode() |
| def encode( |
| self, |
| audio_data: torch.Tensor, |
| audio_lengths: torch.Tensor | None = None, |
| n_quantizers: int | None = None, |
| **kwargs, |
| ): |
| """Encode reference codes, staging only the required modules on CUDA.""" |
|
|
| if not hasattr(self, "_reference_lock"): |
| return super().encode( |
| audio_data, |
| audio_lengths=audio_lengths, |
| n_quantizers=n_quantizers, |
| **kwargs, |
| ) |
|
|
| with self._reference_lock: |
| reference_modules = ( |
| self.encoder, |
| self.quantizer.downsample, |
| self.quantizer.pre_module, |
| ) |
| for module in reference_modules: |
| module.to(self._decode_device) |
|
|
| try: |
| dtype = next(self.encoder.parameters()).dtype |
| audio_data = audio_data.to(device=self._decode_device, dtype=dtype) |
| if audio_data.ndim == 2: |
| audio_data = audio_data.unsqueeze(1) |
| length = audio_data.shape[-1] |
| right_pad = ( |
| math.ceil(length / self.frame_length) * self.frame_length - length |
| ) |
| audio_data = torch.nn.functional.pad(audio_data, (0, right_pad)) |
| if audio_lengths is None: |
| audio_lengths = torch.tensor( |
| [length + right_pad], |
| device=self._decode_device, |
| dtype=torch.long, |
| ) |
| else: |
| audio_lengths = audio_lengths.to(self._decode_device) |
|
|
| z = self.encoder(audio_data) |
| z = self.quantizer.downsample(z) |
| z = self.quantizer.pre_module(z) |
| semantic_z, semantic_codes, *_ = self.quantizer.semantic_quantizer(z) |
| residual_z = z - semantic_z |
| _, residual_codes, *_ = self.quantizer.quantizer( |
| residual_z, |
| n_quantizers=n_quantizers, |
| ) |
| indices = torch.cat([semantic_codes, residual_codes], dim=1) |
| indices_lens = torch.ceil(audio_lengths / self.frame_length).long() |
| finally: |
| if self._decode_device.type == "cuda": |
| torch.cuda.synchronize(self._decode_device) |
| for module in reference_modules: |
| module.to(self._reference_offload_device) |
| if self._decode_device.type == "cuda": |
| torch.cuda.empty_cache() |
|
|
| return indices, indices_lens |
|
|
|
|
| def _tensor_bytes(tensor: torch.Tensor | None) -> int: |
| if tensor is None: |
| return 0 |
| return tensor.numel() * tensor.element_size() |
|
|
|
|
| @torch.inference_mode() |
| def compact_codec_inference_buffers(codec: torch.nn.Module) -> dict[str, Any]: |
| """Remove dead causal masks and bound RoPE tables to configured limits. |
| |
| ``WindowLimitedTransformer.forward`` always constructs an exact mask for |
| the current input and passes it to its parent implementation. Therefore the |
| inherited 32768-square causal mask is not read on this path. Its RoPE table |
| is used, but the configured block size is the model's supported inference |
| limit and is far smaller than the inherited 327680-frame table. |
| """ |
|
|
| from fish_speech.models.dac.modded_dac import WindowLimitedTransformer |
|
|
| records: list[dict[str, Any]] = [] |
| saved_bytes = 0 |
| for name, module in codec.named_modules(): |
| if not isinstance(module, WindowLimitedTransformer): |
| continue |
|
|
| causal_mask = module.causal_mask |
| freqs_cis = module.freqs_cis |
| if freqs_cis is None: |
| raise RuntimeError(f"Codec transformer {name} has no RoPE table") |
|
|
| frame_limit = int(module.config.block_size) |
| if frame_limit <= 0 or frame_limit > freqs_cis.shape[0]: |
| raise RuntimeError( |
| f"Invalid codec RoPE limit for {name}: {frame_limit} " |
| f"of {freqs_cis.shape[0]}" |
| ) |
|
|
| before_mask_bytes = _tensor_bytes(causal_mask) |
| before_rope_bytes = _tensor_bytes(freqs_cis) |
| device = freqs_cis.device |
| module.causal_mask = torch.empty(0, dtype=torch.bool, device=device) |
| module.freqs_cis = freqs_cis[:frame_limit].clone() |
| after_rope_bytes = _tensor_bytes(module.freqs_cis) |
| module._compact_inference_frame_limit = frame_limit |
|
|
| records.append( |
| { |
| "module": name, |
| "frame_limit": frame_limit, |
| "removed_causal_mask_bytes": before_mask_bytes, |
| "rope_bytes_before": before_rope_bytes, |
| "rope_bytes_after": after_rope_bytes, |
| } |
| ) |
| saved_bytes += before_mask_bytes + before_rope_bytes - after_rope_bytes |
|
|
| if len(records) != 3: |
| raise RuntimeError( |
| f"Expected three window-limited codec transformers, found {len(records)}" |
| ) |
|
|
| report = { |
| "policy": "compact_windowed_inference_buffers", |
| "windowed_transformers": len(records), |
| "theoretical_saved_bytes": saved_bytes, |
| "records": records, |
| } |
| codec._compact_inference_buffers_report = report |
| return report |
|
|
|
|
| @torch.inference_mode() |
| def load_compact_codec_model( |
| config_name: str, |
| checkpoint_path: str | Path, |
| device: str | torch.device = "cuda:0", |
| precision: torch.dtype = torch.bfloat16, |
| offload_reference: bool = False, |
| ) -> torch.nn.Module: |
| """Load the pinned codec with compact buffers before CUDA placement.""" |
|
|
| from hydra.utils import instantiate |
| from omegaconf import OmegaConf |
|
|
| from fish_speech.models.dac import modded_dac as modded_dac_module |
|
|
| config_path = ( |
| Path(modded_dac_module.__file__).resolve().parents[2] |
| / "configs" |
| / f"{config_name}.yaml" |
| ) |
| cfg = OmegaConf.load(config_path) |
| if offload_reference: |
| cfg._target_ = "experimental.codec.StagedReferenceCodec" |
|
|
| codec = instantiate(cfg) |
| state_dict = torch.load( |
| checkpoint_path, |
| map_location="cpu", |
| mmap=True, |
| weights_only=True, |
| ) |
| if "state_dict" in state_dict: |
| state_dict = state_dict["state_dict"] |
| if any("generator" in key for key in state_dict): |
| state_dict = { |
| key.replace("generator.", ""): value |
| for key, value in state_dict.items() |
| if "generator." in key |
| } |
|
|
| load_result = codec.load_state_dict(state_dict, strict=False, assign=True) |
| unexpected = [ |
| key |
| for key in load_result.unexpected_keys |
| if not key.endswith(("causal_mask", "freqs_cis")) |
| ] |
| if load_result.missing_keys or unexpected: |
| raise RuntimeError( |
| "Unexpected compact codec checkpoint mismatch: " |
| f"missing={load_result.missing_keys[:5]}, unexpected={unexpected[:5]}" |
| ) |
|
|
| report = compact_codec_inference_buffers(codec) |
| codec.eval() |
| codec.to(dtype=precision) |
| if offload_reference: |
| codec.configure_reference_staging(device) |
| report["reference_path"] = "staged_from_cpu_to_decode_device" |
| else: |
| codec.to(device=device) |
| report["reference_path"] = "resident_on_decode_device" |
| codec._compact_inference_buffers_report = report |
| del state_dict |
| gc.collect() |
| if torch.cuda.is_available() and torch.device(device).type == "cuda": |
| torch.cuda.empty_cache() |
| return codec |
|
|
|
|
| __all__ = [ |
| "StagedReferenceCodec", |
| "compact_codec_inference_buffers", |
| "load_compact_codec_model", |
| "load_reference_audio_soundfile", |
| "warm_reference_encoder", |
| ] |
|
|