fix the three payload failures the pod tests found

None of these were placement or pod bugs: each image booted, pulled its model
and got as far as executing. The remote machinery worked; the payloads did not.

voice — torchcodec arrived as a transitive dependency of f5-tts, PyPI served the
CUDA-13 build, and it died with 'libnvrtc.so.13: cannot open shared object file'
after loading the model. The same class of bug as the torch pin: a CUDA-coupled
package escaping the index. It now installs from the CUDA index with the rest,
resolving to torchcodec 0.11.1+cu128.

audio — 'No module named audiocraft'. audiocraft pins an old torch, so putting
it in the image would drag torch backwards and undo the CUDA pin. transformers
ships MusicGen itself, so a pod serves it through transformers instead; an
install that has audiocraft still uses it. Melody conditioning stays
audiocraft-only rather than silently doing something different.

video — 'TextToVideoSDPipeline.__call__() got an unexpected keyword argument
callback_on_step_end'. Older diffusers pipelines do not take it and raise inside
the call, minutes in, with the model loaded and the GPU paid for. The kwarg is
now added only when the pipeline's signature accepts it. The try/except that
used to wrap it caught nothing: assigning to a dict never raises. This one bites
local generations too, not just pods.
Co-Authored-By: 's avatarClaude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182DhR1eFNPQyGmDrpHbedY
parent 3db44b30
......@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here.
__version__ = "0.2.13"
__version__ = "0.2.14"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even
......
......@@ -109,8 +109,64 @@ def _save_audio_response(audio_data: bytes, ext: str, http_request: Request) ->
return {f"b64_{ext}": b64}
class _TransformersMusicGen:
"""MusicGen through transformers, wearing audiocraft's interface.
audiocraft is Meta's library and is optional on purpose: it pins an old
torch, so installing it into a pod image would drag torch backwards and
undo the CUDA build pin. transformers ships MusicGen itself, so a pod can
serve the model without it — a rented pod failed with "No module named
'audiocraft'" and there was no reason for it to need one.
Only what the generation path calls is implemented: set_generation_params,
generate, sample_rate. Melody conditioning is not — that is audiocraft's
generate_with_chroma, and a caller asking for it gets audiocraft's own
ImportError rather than a silently different result.
"""
def __init__(self, model_name: str, device: str):
from transformers import AutoProcessor, MusicgenForConditionalGeneration
self._processor = AutoProcessor.from_pretrained(model_name)
self._model = MusicgenForConditionalGeneration.from_pretrained(model_name)
self._device = device
self._model.to(device)
self._params = {"duration": 30.0}
def set_generation_params(self, **kwargs):
self._params.update({k: v for k, v in kwargs.items() if v is not None})
@property
def sample_rate(self) -> int:
return int(self._model.config.audio_encoder.sampling_rate)
def generate(self, prompts):
import torch
inputs = self._processor(text=list(prompts), padding=True,
return_tensors="pt").to(self._device)
# transformers counts tokens, audiocraft counts seconds. 50 Hz is
# MusicGen's frame rate, which is what makes `duration` mean anything.
frame_rate = getattr(self._model.config.audio_encoder, "frame_rate", 50)
max_new = max(1, int(float(self._params.get("duration", 30.0)) * frame_rate))
gen = {"max_new_tokens": max_new, "do_sample": True}
if self._params.get("temperature"):
gen["temperature"] = float(self._params["temperature"])
if self._params.get("top_k"):
gen["top_k"] = int(self._params["top_k"])
if self._params.get("top_p"):
gen["top_p"] = float(self._params["top_p"])
if self._params.get("cfg_coef"):
gen["guidance_scale"] = float(self._params["cfg_coef"])
with torch.no_grad():
return self._model.generate(**inputs, **gen) # (batch, channels, samples)
def _load_musicgen(model_name: str, device: str):
from audiocraft.models import MusicGen, AudioGen
try:
from audiocraft.models import MusicGen, AudioGen
except ImportError:
print(f"[audio] audiocraft not installed — serving {model_name} through "
"transformers instead", flush=True)
return _TransformersMusicGen(model_name, device)
name_lower = model_name.lower()
if 'audiogen' in name_lower:
model = AudioGen.get_pretrained(model_name)
......
......@@ -2285,6 +2285,21 @@ def _maybe_i2v_fallback(pipe, kw, mode):
return view, 'ti2v'
def _pipeline_takes(pipe, name: str) -> bool:
"""True when this pipeline's __call__ accepts ``name`` as a keyword.
Diffusers pipelines differ in what they accept, and passing one an argument
it has never heard of fails at call time rather than at setup — the most
expensive moment possible, because the weights are already loaded.
"""
import inspect
try:
params = inspect.signature(type(pipe).__call__).parameters
except (TypeError, ValueError):
return False
return name in params
def _generate_video(pipe, request: VideoGenerationRequest):
mode = request.mode or ('i2v' if (request.image or request.init_image)
else 'v2v' if request.video else 't2v')
......@@ -2336,10 +2351,18 @@ def _generate_video(pipe, request: VideoGenerationRequest):
pass
return callback_kwargs
try:
# Only when the pipeline actually takes it. Older ones (TextToVideoSDPipeline)
# do not, and diffusers raises TypeError inside the call — minutes in, after
# the model is loaded and the GPU is paid for: "TextToVideoSDPipeline.
# __call__() got an unexpected keyword argument 'callback_on_step_end'".
# The try/except that used to be here caught nothing: assigning to a dict
# never raises, and the failure happens later, somewhere else.
if _pipeline_takes(pipe, 'callback_on_step_end'):
kw['callback_on_step_end'] = _vid_step_cb
except Exception:
pass
else:
print(f"[video] {type(pipe).__name__} does not accept "
"callback_on_step_end — generating without step progress",
flush=True)
_apply_camera_motion(kw, request.camera_motion, pipe=pipe)
......
......@@ -46,12 +46,16 @@ COPY packaging/runpod/profiles/core.txt /tmp/core.txt
# local install runs, where cu124 tops out at 2.6 and would put pods years behind
# the transformers-5 code they execute.
#
# torchvision and torchaudio come from the same index here rather than from the
# profiles: installed later from PyPI they would drag a matching CUDA-13 torch
# back in and undo this.
# torchvision, torchaudio and torchcodec come from the same index here rather
# than from the profiles: installed later from PyPI they would drag a matching
# CUDA-13 torch back in and undo this. torchcodec is the one that proved the
# point — it arrived as a transitive dependency of f5-tts, PyPI served the
# CUDA-13 build, and voice cloning died on a rented pod with "libnvrtc.so.13:
# cannot open shared object file" after loading the model. Anything linked
# against CUDA belongs on this line.
RUN --mount=type=cache,target=/root/.cache/pip \
python -m pip install --upgrade pip \
&& python -m pip install --index-url ${TORCH_INDEX} torch torchvision torchaudio \
&& python -m pip install --index-url ${TORCH_INDEX} torch torchvision torchaudio torchcodec \
&& python -m pip install -r /tmp/core.txt \
&& python -c "import torch; print('torch', torch.__version__, 'cuda', torch.version.cuda)" \
&& rm -f /tmp/core.txt
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment