feat(loader): expose audio_tokenizer_device option for memory-tight GPUs

Add a new audio_tokenizer_device input on MOSSTTSLoader (auto/cuda/cpu).
auto mirrors the main model's device (previous behaviour). cpu keeps the
audio tokenizer on host RAM, saving ~1-3 GB of VRAM at the cost of some
CPU latency at the start (encoding the reference clip) and end (decoding
the generated codes) of each generation.

Useful on 16-20 GB GPUs where MOSS-TTS' main model + activations push
right against the ceiling. Including the audio_tokenizer_device in the
cache key prevents stale entries when a user toggles the setting.
This commit is contained in:
Federico Volovnik-Jose
2026-04-27 15:04:44 +02:00
parent 39ce51f847
commit f6e5d420a1
2 changed files with 28 additions and 6 deletions
+20 -3
View File
@@ -267,14 +267,31 @@ def get_or_load(
dtype_str: str,
attn_impl: str,
keep_loaded: bool = True,
audio_tokenizer_device: str = "auto",
) -> dict:
"""Load or fetch the cached MOSS-TTS model + processor."""
"""Load or fetch the cached MOSS-TTS model + processor.
audio_tokenizer_device: where to place the audio_tokenizer (a separate
small model used to encode reference audio and decode generated codes).
"auto" mirrors the main model's device. "cpu" keeps it off the GPU,
saving ~1-3 GB of VRAM at the cost of some latency at the start and
end of each generation. Useful on memory-tight GPUs.
"""
_ensure_atexit()
resolved_device = resolve_device(device)
resolved_dtype = resolve_dtype(dtype_str, resolved_device)
resolved_attn = resolve_attn_impl(attn_impl, resolved_device)
resolved_at_device = (
resolved_device if audio_tokenizer_device == "auto" else audio_tokenizer_device
)
key = (model_id, resolved_device, _resolved_dtype_str(dtype_str, resolved_device), resolved_attn)
key = (
model_id,
resolved_device,
_resolved_dtype_str(dtype_str, resolved_device),
resolved_attn,
resolved_at_device,
)
if key in _MODEL_CACHE:
entry = _MODEL_CACHE[key]
entry["keep_loaded"] = keep_loaded
@@ -288,7 +305,7 @@ def get_or_load(
processor = AutoProcessor.from_pretrained(model_id, trust_remote_code=True)
_fix_moss_model_config_token_ids(processor)
if hasattr(processor, "audio_tokenizer") and processor.audio_tokenizer is not None:
processor.audio_tokenizer = processor.audio_tokenizer.to(resolved_device)
processor.audio_tokenizer = processor.audio_tokenizer.to(resolved_at_device)
if resolved_device == "cuda":
torch.cuda.empty_cache()
+8 -3
View File
@@ -21,6 +21,10 @@ class MOSSTTSLoader:
{"default": "sdpa"},
),
"keep_loaded": ("BOOLEAN", {"default": True}),
"audio_tokenizer_device": (
["auto", "cuda", "cpu"],
{"default": "auto"},
),
}
}
@@ -30,21 +34,22 @@ class MOSSTTSLoader:
CATEGORY = "MOSS-TTS/loaders"
@classmethod
def IS_CHANGED(cls, model_id, device, dtype, attn_implementation, keep_loaded):
def IS_CHANGED(cls, model_id, device, dtype, attn_implementation, keep_loaded, audio_tokenizer_device):
# When keep_loaded is False, the previous run's Generate node freed the
# model from the cache. ComfyUI's per-node output cache would otherwise
# hand the now-empty handle to Generate again. Returning a fresh value
# each call forces ComfyUI to re-execute load() and reload the model.
if not keep_loaded:
return time.time()
return f"{model_id}|{device}|{dtype}|{attn_implementation}|keep"
return f"{model_id}|{device}|{dtype}|{attn_implementation}|{audio_tokenizer_device}|keep"
def load(self, model_id, device, dtype, attn_implementation, keep_loaded):
def load(self, model_id, device, dtype, attn_implementation, keep_loaded, audio_tokenizer_device):
entry = get_or_load(
model_id=model_id,
device=device,
dtype_str=dtype,
attn_impl=attn_implementation,
keep_loaded=keep_loaded,
audio_tokenizer_device=audio_tokenizer_device,
)
return (entry,)