- 29 Oct, 2026 4 commits
-
-
Stefy Lanza (nextime / spora ) authored
Two bugs that only bite when something else has already gone wrong, so they turned a recoverable load into a failed one. A fully quantized module could not say what dtype it was. transformers has next(param.dtype for param in self.parameters() if param.is_floating_point()) and a bnb 4-bit module has NO floating-point parameters -- Params4bit are uint8 -- so the generator is empty and next() raises StopIteration. diffusers' DiffusionPipeline.to() reads module.dtype unguarded while deciding whether to warn about fp16-on-cpu, so it escapes enable_model_cpu_offload(); and because loads run in a worker thread, Python turns it into RuntimeError: StopIteration interacts badly with generators and cannot be raised into a Future failing the whole load. Measured on LTX-2.5, whose text encoder is 4-bit: the resident attempt did not fit, the fallback to model CPU offload hit this, and the load restarted from scratch. The recovery path broke exactly when it was needed, and it would do so for any pipeline with an all-4-bit component. The property asks what dtype a module is, and a quantized module has a good answer: the dtype of its storage. The shim falls back to the first parameter of any kind, then to float32, and only when the original raises -- a module with floating-point parameters is answered exactly as before. Guarded at every enable_model_cpu_offload() call site, with a test that fails if one is added without a guard. The injected components kept the GPU between attempts. They are pre-built objects held in a local dict and handed to each attempt through _with_quant, so freeing the pipeline frees nothing: the dict is still a strong reference and whatever the failed attempt moved onto the card stays there. [video] teardown-leak breaker: 16.9 GB of this pipeline's tensors survived teardown; nulled 7581 external reference(s); holders: ["2.01 GB dict['weight', 'bias']", ...] The holders are the modules' own _parameters dicts -- the modules outlived the pipeline. The breaker got the VRAM back by stripping them field by field, which works, but it guts components the next attempt is about to reuse. They are now moved to the CPU at both resident-failure exits: moved, not dropped, because the retry needs these same components, which is the whole point of injecting them. The breaker stays as the backstop for a component that refuses .to('cpu'). Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
Letting a sized-to-fit cache hit try the GPU (1f42d08c) is right for a model that fits and expensive for one that does not. Measured on LTX-2.5: 04:55:07 RESIDENT all-GPU load (sized to fit) 04:56:53 [resident] components left off-GPU: transformer(CUDA OOM), vocoder(CUDA OOM), prompt_enhancer(CUDA OOM) 05:11:10 ✓ loaded — strategy: model CPU offload 05:11:12 23.28 GB total, of which 21.32 GB offloaded to host RAM Sixteen minutes to reach exactly the placement that would have been chosen immediately, and it would have paid that on every load. The sizing was not wrong so much as answering a different question: it knows what the weights weigh (22.1 GB against 25.0 GB free), while resident needs every component -- transformer, vocoder, VAE, text encoder, prompt_enhancer -- plus the activation peak, all at once. The real footprint turned out to be 23.28 GB. Only an attempt can answer that, so the answer is recorded as a system-owned learned field beside measured_n_gpu_layers, which exists for the same reason: stop repeating an expensive discovery every load. Both outcomes are recorded, not just the failure. Writing only failures would re-prove a model that fits on every load; writing only successes would never stop the expensive retry. And it is not a one-way door -- a later success clears an earlier failure, because a bigger card, a smaller quantisation or fewer co-resident models all change the answer. Unknown still means try. A model never attempted, or a card that has since changed, gets its chance at the GPU rather than being written off by default. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
XTTS-v2 and Parler were advertised in /v1/models and failed on every request. Three separate causes, none of them the one the report assumed. A venv built by a DIFFERENT interpreter bricks the engine permanently. A worker venv keeps bin/python as a symlink to the python that created it, so one built on the host and used from the container (or built before the image moved its interpreter) leaves a dangling link. The failure is not a clean "missing": Path.exists() follows symlinks and says False, so the code decides the venv is absent and runs `python -m venv` over the top, which trips on that same dangling bin/python3: Error: [Errno 2] No such file or directory: '.../parler_venv/bin/python3' reported for weeks as "the venv can't be created", with a fully populated venv and 7 GB of packages sitting right there. Both workers now check that the interpreter RUNS, not that the path exists, and clear a foreign venv before recreating. TRANSFORMERS_CACHE pointed at a second cache. It is deprecated but transformers 4.x still honours it, and honours it as the cache_dir handed to hf_hub_download -- a hub-layout cache, not a different kind of store. Setting it to <dir>/transformers therefore gave the machine TWO hub caches: measured, <dir>/hub held 122 models and <dir>/transformers held 2. A worker in an isolated venv read only the second, found nothing, and -- forced offline so a missing model fails fast -- reported "We couldn't connect to https://huggingface.co" for a model that was present the whole time. It is also why workers re-downloaded models the server already had. All four variables now name the hub directory. XTTS never built its venv at all. parler bootstraps on first use; XTTS only looked for a venv and raised a 501 telling the operator to go and make one. Same situation, two behaviours. It bootstraps now, with CODERAI_XTTS_AUTOINSTALL=0 for a host that manages its own venvs. Also the voice listing the report asks for, but NOT at /v1/audio/speakers: that path is already enrolled speaker voiceprints for recognition, and serving TTS voices there would conflate two unrelated things. The voices join the other "what can this model do" answers at /v1/audio/speech/capabilities, which already takes the same model= parameter. It never loads a model to answer -- an already-loaded backend is asked directly, otherwise the list is whatever was remembered the last time that model loaded, and voices_source says which. And it does not invent metadata: Kokoro ids genuinely encode accent and gender (af_sarah -> en-us, female) so those are derived, while an XTTS speaker name carries neither and reports None. A wrong gender against a voice is worse than no gender. voice_selection is reported too, because parler has no speaker names at all -- it is steered by a free-text description, and a client reading only an empty voices list would conclude it cannot be directed. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
A production run put 93% of a video model on the CPU beside an idle GPU, nine times. The ladder was not at fault -- it evicted an active model, then a keep-resident one, reached 24.9 GB free and concluded: Full-GPU load looks viable (22.1 GB peak need, 24.9 GB free) — using full GPU and two minutes later the same load reported: [pipeline-cache] injected cached components → forcing hook-based 'model' CPU offload Video load strategy: model CPU offload [after load] VRAM 0.4/25.3 GB (24.9 GB free) footprint 22.11 GB total, of which 20.47 GB was offloaded to host RAM The sizing expressed "use the whole card" by leaving `offload` as None, and None also means "nobody chose". The cached-components path could not tell the two apart, so it fell through to its own default -- 'model' CPU offload, which is there for a dual-expert Wan2.2 whose two ~10 GB experts genuinely do not fit in 24 GB, and which was then applied to every cached pipeline whatever its size. It did not heal either: a pipeline loaded under accelerate offload is never re-cached ("cache must be built from a pre-offload pipeline"), so once a model had been cached, every later load was forced onto the CPU and no clean copy could replace it. The verdict now travels with the request. A cache hit that was sized to fit loads resident; the dual-expert default still applies when it does not fit, and video_resident_experts still forces it on. _full_gpu_viable is assigned unconditionally. Written where the sizing happens it would have lived inside `if _offload is None:` → `try:` → `if cuda.is_available():`, so a per-model strategy, a CPU-only host or any exception there would raise NameError and break EVERY video request -- a much worse failure than the one being fixed. The test asserts the assignment DOMINATES the read, not merely that it precedes it, and fails if it is nested back inside a branch. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
- 28 Oct, 2026 31 commits
-
-
Stefy Lanza (nextime / spora ) authored
Production, 2026-10-28: Krea-2-Turbo answered one request and then 500ed. RuntimeError: Expected all tensors to be on the same device, but got index is on cuda:0, different from other tensors on cpu The offload moved the encoder to the CPU and the pipeline is CACHED, so the next request met an encoder that was no longer where encode_prompt indexes. _free's docstring even said the encoder "waits in host RAM ... the next request needs it again" -- and nothing ever brought it back. What turned that into a 500 rather than a skipped optimisation: the side effect outlives this function. The bail-out path returns the original kwargs and lets the pipeline generate "the ordinary way", but the ordinary way calls the SAME encoder, so it hit the same mismatch and the request failed anyway. A fallback that leaves the thing it fell back to broken is not a fallback. So the offload is a per-request cycle -- bring the encoder on, encode, move it off -- and every bail-out restores the encoder first, because each one hands the request to a path that needs it on the GPU. The regression test drives a pipeline that really refuses to encode off-device and asserts the SECOND request still works; without the restore it reproduces the production message exactly. A second test pins the weaker promise that any bail-out leaves the encoder where the ordinary path can use it. Found by the operator mid-render, not by the suite: every check so far ran one request per model against a freshly started engine, which is the one case that cannot fail. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
Scaleway, Exoscale, OVHcloud and Hyperstack all rent GPUs by the hour or the second, and nothing about the fleet logic is really RunPod-shaped. Written down now so the seam does not have to be re-derived later; none of it is built. The point worth keeping: the provider-specific part is small. Demand-based growth, the reaper, budgets, the ledger, spillover, shared pools, LoRA fan-out and region selection all sit above the provider and ask it for about eight things, which RunpodClient already is -- ping, a catalogue with VRAM and price, create, get/list, wait_ready, stop/terminate, logs, volumes. A second provider is a second client plus a per-pool choice, not a second orchestrator. The section also lists what will NOT port, because the §10 notes were all found live and most are categories rather than one provider's quirk: an undocumented image size ceiling, a boot budget that has to cover the image pull, how the port is reached (proxy vs direct TCP vs a load balancer with its own idle timeout), rate limits in their own units, terminal states that do not look terminal, and region-scoped storage. Indexed from docs/README.md so it is findable without reading to the end of an 890-line file. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
The sequential text-encoder offload reported itself "not enabled" on a card that plainly had `sequential_text_encoder = True`. Four defects, stacked so each one hid the next; the first is much wider than the feature that exposed it. _resolve_config_key could not resolve ANY non-text model by alias. Typed entries are keyed "<kind>:<name>", so the exact-id and alias-map lookups both miss, and the basename fallback misses too: "video:LTX-2.3-PinkCherry" has no slash, so basename() hands back the whole prefixed string. Every per-model image / video / audio / tts / embedding setting was therefore replaced by the global default, silently, for any model addressed by its alias -- which is the only name that distinguishes two configs sharing one checkpoint path. The resolver now tries the typed keys, ordered by the types the model is actually registered as. images.py (6 sites) and tts.py looked their config up with a raw config.get(model_key) and had the same bug; they resolve now, with a test that fails if a raw one reappears. The flag itself lived only in _raw_cfg, where a consumer reading config['sequential_text_encoder'] never saw it. Promoted to a top-level runtime kwarg, as gpu_split beside it already documents. With the gate finally firing, the render failed: "Must provide `prompt_attention_mask` when specifying `prompt_embeds`". encode_prompt's return order had been guessed. Read off the installed sources, LTX-2 returns (embeds, mask, neg_embeds, neg_mask) and Krea 2 returns (embeds, mask) -- the mask comes SECOND, and the old code read it third, filing negative embeddings as the attention mask. Naming is now done by classifying the values (a mask is an integer tensor, embeddings are floating) instead of by position. That rewrite then dropped Krea 2's negative prompt, which would have been worse than the failure it replaced: Krea 2's __call__ encodes "" itself when handed no negative, against an encoder this function has just moved to the CPU. The negative is always encoded up front for those families now. Hardened for families nobody has tried: an SDXL-shaped pipeline returns four float tensors including the pooled pair, and naming only two does not degrade quietly -- the pipeline rejects incomplete embeddings and the request fails. If everything encode_prompt returned cannot be named, the optimisation is abandoned for that request and says so. Measured on LTX-2.3-PinkCherry (480x480, 25 frames, 8 steps, RTX 3090): without 369.4 s peak 23718 MiB 91% util with 345.3 s peak 24098 MiB 92% util 7.3 GiB freed Read as headroom, not speed: the card is 24 GiB and the denoiser fills what it is given, so peak sits at the ceiling either way and the 24 s is one sample. The 7.3 GiB is what stops these renders being pushed to CPU offload, where the same job took tens of minutes. Also here: a clip-audio scene no longer needs an explicit duration (B1 said the duration comes from the clip, and validation refused exactly the request the feature exists for), and _free now reports per component whether it moved, was already off the GPU, or REFUSED the move -- a bnb-quantized encoder can refuse, and all three used to arrive as "nothing was freed". test_sequential_text_encoder's fakes returned bare strings in the wrong tuple order, which is why the bug shipped: the test encoded the mistaken belief rather than the pipeline's contract. They carry dtypes and the real orders now. test_compose's engine-selection test patches shutil.which as well, because the production image ships wav2lip and sadtalker shims on PATH and the test was asserting the absence of a binary the real deployment has. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
A text encoder runs ONCE per request, before the first denoising step, and is dead weight for every step after -- but it stays resident and keeps its VRAM. Measured on the cached snapshots, that is not a rounding error: Z-Image's encoder is 7.5 GiB of a 19.2 GiB pipeline, and LTX-2.3's 4-bit Gemma-3-12B is ~6.5 GiB in front of a 15 GiB transformer. PinkCherry declaring exactly 24.0 GB on a 24 GiB card is that sum, and it is why every LTX render was pushed to CPU offload. Encoding first and freeing the encoder hands that VRAM to the denoiser, so a model that "did not fit" fits. This is the sequential text-encoder offload the requirements asked for; it existed only as a side effect before, because diffusers' enable_model_cpu_offload happens to move each component out after use -- so the OFFLOADED path had it and the resident path did not. The embedding argument names differ per family and there is no common base class to ask, so they are read from the pipeline's own signature: Krea 2 takes prompt_embeds + prompt_embeds_mask and needs a second call for the negative, LTX-2 takes prompt_attention_mask and encodes both at once. A family that takes no embeddings at all is left alone rather than having its encoder freed with no way to pass the prompt back. Opt-in per model and settable from the model page, because freeing the encoder costs a host-RAM copy and only pays where the card is tight. The encoder is moved to CPU rather than deleted: the pipeline is cached and the next request needs it again. Any failure returns the request untouched -- a VRAM optimisation must never be the reason a generation fails, and a test holds that. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
Two bugs of the same shape: a signal that already existed and was not consulted. Same-path siblings. Two cards can share one weights path and differ only in settings -- Wan2.2-I2V-A14B-Lightning and the plain Wan2.2-I2V-A14B both point at Wan-AI/Wan2.2-I2V-A14B-Diffusers, declaring 22 GB and 151 GB. The manager has disambiguated exactly this since the lisa/lisa-32k case, through `requested_id`, and NEITHER placement caller passed it. So the Lightning card was sized by its 151 GB sibling, read as ~127 GB over free VRAM -- far outside the borderline band -- and went straight to CPU offload without trying the card, where it computed at 0% GPU for 26 minutes holding 37 GB of host RAM. Measured: 97.4 GB without the id, 14.1 GB with it, which is the difference between exile and residency on a 24 GiB card. Quantizing the experts was necessary but not sufficient; the ESTIMATE was still the wrong card's. The leak watcher's load signal. _load_in_progress() consulted only the manager's ready event, which diffusers image/video loads never touch -- they load in codai/api/{images,video}.py. A 12-minute multi-shard load therefore looked idle, its monotonically rising RSS looked exactly like a leak, and the mitigation ladder ran against a load doing nothing wrong ("RSS 19.8/43.0 GB LEAK SUSPECTED", every poll). It now also consults the loading reservations, which cover those paths by construction. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
diffusers added `callback_on_step_end` per pipeline rather than across the board, and audio_gen passed it unconditionally. StableAudioPipeline does not take it, so every request raised StableAudioPipeline.__call__() got an unexpected keyword argument 'callback_on_step_end' AFTER the model had finished loading -- one unsupported keyword cost the whole load and read like a broken model rather than a wrong argument. Asked of the signature, not kept as a list of pipeline names: such a list is wrong again at the next diffusers release. The pre-0.25 `callback` spelling is adapted so those pipelines keep their progress, and a pipeline that supports neither says so, because silently losing progress reporting and mid-run cancellation should not be invisible. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
Offload: the default becomes auto-borderline, not auto. `auto` condemns a model on its ESTIMATE -- if used_vram_gb exceeds free VRAM it goes straight to CPU offload without ever trying the card -- and that estimate is a measurement from an earlier load in some cards and a hand-entered round number in others (LTX-2.3-PinkCherry declared exactly 24.0 on a 24 GiB card). So a conservative figure permanently exiled a model that would have fit. The cost of being wrong is asymmetric, which is what decides it: guessing "fits" wrongly costs one failed attempt and a fallback, while guessing "does not fit" wrongly costs a CPU copy of every component and a PCIe trip per request for the life of the install -- 28-37 GB of host RAM per video render on a 54 GB box with no cgroup limit to stop it. The dropdown is relabelled by intent and reordered, because the old labels were the trap: `auto` read as the sensible choice while being the pessimistic one. Tests: three files hardcoded /storage/coderai, so collection FAILED outright inside the production image and every test in them had only ever been exercised against the host's dependency set -- which is diffusers 0.37.0.dev0 with no torchsde and no PyAV, against the image's 0.42.1 with both. The four Krea 2 edit tests were silently skipping for want of Krea2Pipeline and now run (15 passed, 0 skipped in-image). Paths are derived from __file__ instead. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
The pin to native (SDPA) is also the floor on image generation speed, and that is worth stating where it happens. Measured 2026-10-28 on Z-Image-Turbo-IntoRealism, 768x1344, 10 steps, idle RTX 3090: 21.4 s warm, 2.1 s/step. Two of the three things worth suspecting are ruled out rather than assumed -- the card's step count IS applied (the per-request progress endpoint reports total=10) and there is no bnb repack (the checkpoint is fp8 upcast to bf16) -- and the third was the cause: an unnecessary enable_model_cpu_offload from the model page's default offload_strategy, on a model that fits the card whole. 72 s -> 33 s -> 21.4 s. What remains is SDPA, and for this family that is not a choice: Z-Image and Krea 2 mask their text conditioning, and neither SageAttention nor flash-attn 2 accepts an attn_mask (_ATTN_MASK_FAMILIES in codai/api/video.py, measured on LTX-2.3-10Eros which failed on sage then identically on flash). A few seconds per megapixel is the hardware here, not a misconfiguration. The remaining opportunity is named and NOT taken: this pins every image pipeline, including families that never mask and could use a fast kernel. The video path already decides that per pipeline class; doing the same here needs the masking image families enumerated and each measured, because getting it wrong is a 500 at the first denoising step rather than a slow render. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
The diarization worker resolved an HF token from the environment or a protected file in the config dir, and gated pyannote repos worked. The MODEL LOADING path had no such resolution: the host runner forwards HF_TOKEN from the host environment, which is only set if whoever launched the container exported it, and on this install it was not -- so huggingface_hub went out unauthenticated for every from_pretrained. That is invisible until a repo is gated, and then it fails as 401 "must be authenticated", which reads exactly like terms that have not been accepted even when they have. On stabilityai/stable-audio-open-1.0 the account had access and the token on disk could fetch the weights with curl, while the engine loading them could not -- so the obvious reading was a licence problem, and it was a credentials-plumbing problem. The resolution moves into codai/hf_cache.py, which already owns how a process talks to the Hub, and is applied from the same entry point as the cache settings: both have to be in place before huggingface_hub is imported, and an install that configured one has configured the other in the same directory. The file/env list is kept identical to the worker's, and the worker now calls it, because two answers to "where is the token" is how one path authenticates and another does not. Its per-request override still wins. Never logged: it ends up in request headers, and a test holds that. Also torchsde, which Stable Audio Open's CosineDPMSolverMultistepScheduler is built on. diffusers imports it lazily, so without it the model downloads its 3.8 GB in full and only then fails at request time, after the wait. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
--oom-score-adj only decides WHO the kernel reaches for once the host is already out of memory. It does not stop a container getting there, and on 2026-10-28 nothing did: an engine grew to 46.3 GB of a 54.8 GB host, pushed 20.4 GB into swap, and the OOM killer never fired at all (oomkill 0) -- the box thrashed through 108 million page-ins until it was reset by hand. A livelock is worse than a kill: a killed engine restarts, a thrashing host serves nobody and cannot even be logged into. --memory alone still permits swapping, which IS the thrash, so --memory-swap is set to the same value: exceeding the limit becomes a kill rather than a slow death. The default is the host's RAM less a reserve for everything that is not coderai, with an 8 GB floor, because a limit under what one model needs would fail every load -- a worse failure than the one this prevents. It sits ABOVE coderai's own softer ceiling (offload.max_ram_gb, which drops caches and evicts idle models at 90% of it), so the app tidies up first and this is only reached when something is genuinely wrong. On the host that prompted it: evict at 38.7 GB, app ceiling 43 GB, container killed at 48 GB, 6 GB left for the OS. The limit is applied only when the kernel can actually enforce it. A limit it cannot is worse than none: it is discarded with a warning on every start while an operator reading "memory limit: 48g" reasonably believes they are protected. That host has no memory cgroup controller at all (cgroup v1, no `memory` in /sys/fs/cgroup), which is also why its OOM killer stayed silent -- with no accounting there is no limit to reach -- so it now says so, and says what to boot with instead. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
LTX-2.x is the one video family here with an audio VAE and a vocoder, so its output is LTX2PipelineOutput(frames=..., audio=...) -- audio generated jointly with the frames and therefore actually in step with them. Only `frames` was read, so every LTX render came out silent and the only way to get sound was to generate some separately with another model and mux that on top, which does not match the picture. The generated track is now muxed in BEFORE the other post-processing steps, because every one of them re-encodes the frames and the track belongs to these exact ones. `add_audio` becomes tri-state so "say nothing" and "say no" differ: unset keeps what the model generated, false gives a silent video, true runs the older separate-generation path. It defaulted to False, which cannot be told from an explicit false -- so a straight default would have dropped the soundtrack. None is falsy everywhere False was, so existing callers are unaffected. Found while testing it: _has_audio tried `import av` under a bare `except: return False`, and PyAV is in no main requirements file -- only the isolated LongCat venv's. On an install without it EVERY file answered "no audio", and because it gates _mux_original_audio, a clip's own sound was silently dropped from every upscale and interpolate. The audio was there, nothing could see it, and nothing said so. ffprobe cannot be missing the way PyAV can, since ffmpeg is what does the muxing, so it is the fallback, and a failure of both is logged instead of reported as silence. Also: one of my own tests asserted a minimum number of ffmpeg progress reports. `-progress` emits on a time interval, so a small encode can finish having emitted a single block -- it passed alone and failed under the full suite. It now asserts what is guaranteed: frames counted, counted upward. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
-
Stefy Lanza (nextime / spora ) authored
Krea2Pipeline.__call__ takes a prompt and no image, and the family's only other entry points in diffusers 0.42 are the Krea2Turbo modular blocks. So the card with the identity-edit LoRA had its weights ready and no way to be asked "change THIS picture" -- and because _task_of() finds no img2img mapping for the family, an edit request fell through to text-to-image and silently ignored the image it was given. The edit is done IN CONTEXT, not by denoising from the reference: the reference is VAE-encoded once, kept clean, and appended to hidden_states beside the canvas, so identity survives the whole trajectory instead of being worn away as the canvas resolves. That is what the family's edit LoRAs are trained against, and it is why `strength` has no meaning here -- there is no partial noising to control, which also makes _is_instruction_edit classify it correctly straight off its signature. Telling the token groups apart is the whole trick. position_ids is (seq, 3): the canvas keeps (0, h, w) and the reference repeats its grid at plane 1, so the rotary embedding separates "this pixel of the canvas" from "this pixel of the reference" while keeping them aligned. Identical coordinates would make the two indistinguishable. The builder is module-level and pure so it can be checked without the 12B weights, and on a build whose diffusers has no Krea 2 at all -- importing the module never breaks such a build. Built with from_pipe over the loaded components: a Krea 2 transformer is 12B and a second load would mean an eviction on every alternation between generating and editing. The reference is encoded with mode(), not sample(), so the same edit of the same picture does not differ run to run. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
13 (per-request progress), 13b (encode frames + the job log), A4 (72s -> 21.4s per still), plus models.civitai_cache_dir. B2, B3 and B4 were already implemented and are verified live. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
13. A request may name itself (`request_id` in the body, or an X-Request-Id header) and poll /v1/{images,video,audio}/progress?request_id=... for its OWN steps. One global slot per media type meant two callers could not tell their progress apart, so a client stopped reading them altogether and showed no step progress at all while the server was drawing. The name is used as the TASK id, so this is a VIEW over the existing task registry -- which already tracks step/total/status per task under a lock and prunes finished ones -- rather than a second place progress is written. Claimed once, because a request registers a short "loading" task as well and both adopting the id would serve the client whichever happened to be last. Unknown id = 404, so a client stops polling on a definite answer instead of reading somebody else's progress. 13b. The encode reports frames written (`encoding 1200/2400 frames`) across the band it owns, so the longest step of a render stops looking like a stall; and the job's own log, kept all along but unreachable, is served as a tail in the status document plus GET /v1/video/compose/{id}/log?since=N for the whole thing incrementally. The record holds the live list, not a snapshot. A4. 72s per 768x1344 still -> 21.4s, measured on an idle 3090. The card carried `offload_strategy: "model"`, which is the model page's DEFAULT, so enable_model_cpu_offload() ran on a model that fits a 23.56 GB card whole and paid PCIe for it on every request. `auto` loads on GPU and falls back to offload only on OOM. The doc's other hypothesis is ruled out rather than assumed: the live progress endpoint reports total=10, so the card's step count IS applied, and an explicit steps=10 takes the same 21.2s. Also: `models.civitai_cache_dir`, configured like hf_cache_dir and gguf_cache_dir because it is the same kind of thing. Unset, these multi-gigabyte checkpoints land beside the other caches -- on this host a 140 GB volume, which a 13.5 GB checkpoint took to 99% until the next model could not write even its config fetch ("No space left on device"). That is what was actually stopping Z-Image-Turbo-IntoRealism from loading at all. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The whole suite green: 5507 passed, 0 failed. Two real defects came out of it (a refusal that named neither the capability nor the fix, and a PackageNotFoundError where a missing bitsandbytes should be reported), the rest were expectations left behind by changes to the product. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Every one encoded something that had since changed, or depended on a package this checkout does not have. Each was read against the code before being touched, and two turned out to be real defects: - ggml_rpc.available() returned the bare exception, so a model naming RPC servers refused with "No module named 'llama_cpp'" -- true, but naming neither the capability nor the fix. Every refusal now says both. - building a BitsAndBytesConfig without bitsandbytes installed raised PackageNotFoundError from importlib.metadata. The IMPORT is guarded; only construction consults metadata, so a Vulkan-only or CPU install got a traceback naming neither the model nor the setting. Now reported as loudly as an unrecognised mode, because the consequence is the same: full precision. The 2-bit path already did this for optimum-quanto. The rest were stale expectations: - the broker protocol inverted (the client speaks first: one read, no separate ack), gained keepalive pings and an immediate `pending` frame, and renamed its stream envelopes to chunk/done. Added coverage for the status gate that suppresses `pending`, which nothing tested. - /admin/api/status was removed in def78c18 and the front owns status now, so its on-the-way-out coercion had nothing left to coerce. Retargeted to the invariant that survives: entries are well formed when MADE. - main()'s default boot became the front/engine split, so the app instance reaches uvicorn in the ENGINE path now. And four were brittle rather than wrong, each failing for something it was not testing: a 4000-character source window that stopped covering the raise when a comment was added above it (now the function's own source); a hand-written Config stand-in that broke on every new section the product read (now falls back to the real defaults, per section); a generic `selectedId` used as a proxy for "the pipeline UI is gone" that unrelated code then adopted; and a pool test that needed pypdfium2 to exercise what a BUSY pool does. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Section 14 of the Motus list: 14a (AI music never reached compose), 14c (wrap text instead of shrinking it below readable), 14d (a clip's sound from its own file). 14b was fixed in 0.5.12 by making an image load visible to the eviction ladder. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
14c. A text overlay was one drawtext line sized from the frame height, so a long text ran off a vertical frame, and a pre-wrapped text was drawn as a single line because an explicit newline was never a break. `overlays[].fit` takes none (the default, today's behaviour) | wrap | shrink, and a newline now breaks whatever the fit. Wrapping emits one drawtext per line, placed as a single left-aligned block measured from the widest line, rather than letting each line centre itself ragged. `captions.fit` takes shrink (the default) | wrap, folding a too-wide row's trailing words onto a new row instead of shrinking that event -- on a vertical video shrinking can go below readable. Word order is preserved, so the karaoke highlight still walks the words in the order they are spoken. Both fitters now MEASURE the result instead of scaling the size by the width ratio. Advance is very nearly linear in size, which is why the shortcut looks right, but hinting rounds each glyph's advance to whole pixels and the error compounds over a long string: a 95-character line computed to fit at size 8 measured 440 px against a 300 px budget. A test caught it. _fit_size carried the same assumption and shrink is its default, so it is fixed the same way. 14d. `visuals[].audio_src` names a separate audio-only file -- DASH sites serve picture and sound as separate streams, so the best video of a clip often has no audio track and a caller that cannot mux had to skip it. The picture path is deliberately left alone: stage 3 reuses it as the visual's video, and an audio-only file there would render a scene with no image. Lengths in audio_mix="clip" come from whatever carries the sound. Also: the dialogue tests no longer depend on a TTS model being installed. They test how a dialogue scene is VALIDATED and say so -- their portraits are inline "so the test does not depend on what is saved on this install" -- but three of them failed standalone with 501 "narration needs a TTS model" without reaching the validation they exercise. Only the inventory is faked, not the check. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
audio_generate answers an AudioGenerationResponse, a pydantic model, and _generate_music read it with .get(). Every compose asking for AI music raised "'AudioGenerationResponse' object has no attribute 'get'", warned "rendered without music", and then SUCCEEDED -- silent. That is why it lasted: the render never failed, so only someone watching the finished video would notice, and "AI composed" music has been off in production the whole time. Both shapes are accepted, since the same call is reachable through paths that answer a plain dict, and a non-dict item is coerced rather than assumed. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Four fixes found from one question about two stray ffmpeg processes: - an audio track is built at its length instead of padded up to it, so the mix cannot hang at 100% CPU (4/10) or silently come out a clip short (3/14) - an ffmpeg we start can no longer outlive us (PR_SET_PDEATHSIG) - an image load reserves its VRAM for its load window, so it is visible to the eviction ladder instead of handing out memory it has already taken - the dtype default is chosen from a device that has been detected, so a text model stops loading fp32 on a CUDA card -- twice the VRAM, and no Flash Attention, which is why Qwen3.5-9B answered nothing since 2026-09-30 Plus `precision` is settable from the model page, where the quantization it belongs with already was. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
run() kills its child on cancel and on timeout, but both need this process to be alive to act. SIGKILLed -- an OOM, a container stop, an interrupted test run -- no finally block runs, the child is reparented to init, and nothing owns it any more. That is not a dormant process: a filter graph that never reaches EOF spins at 100% CPU indefinitely. Two orphans from interrupted test runs were found on a dev host having burned 15 h and 11 h of CPU, with no parent to attribute them to. PR_SET_PDEATHSIG has the kernel kill the child as the parent dies, so the outcome stops depending on our cleanup running at all. Best effort, a no-op where prctl is unavailable, and tested by SIGKILLing a real parent rather than by asserting the flag is set. Also bound the audio mix at 20x its own length with a two-minute floor. Mixing a few seconds of PCM takes milliseconds, so the two-hour video backstop meant a wedged mix sat for two hours; it now gives up in two minutes. Plus a test that every name identifying a whisper.cpp runner reaches it -- the runner's id, a shared alias, and the weights path /v1/models advertises, by full path, basename and stem -- and that naming the weights spreads across a shared alias instead of serialising a pool configured for capacity. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
`precision` was read by the loader and listed in main.py's _LOAD_AFFECTING, but it was in neither the admin save allowlist nor the model page, so the one setting that decides how much VRAM a card actually needs could not be configured anywhere. The dtype then came from a default derived from GPU detection -- and a card's precision is a property of the card, not of the hardware it happens to land on. The select sits with the quantization boxes because that is where it is read wrongly: 4-bit and 8-bit quantize the WEIGHTS, while the embeddings, lm_head, the norms, the activations and the KV cache all stay in this dtype, so a 4-bit model left in fp32 still pays fp32 for every one of them. The hint says so, and says that fp32 also rules out Flash Attention. "" is stored as null, so "not configured" stays distinguishable from a value and the loader's own default still applies. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
NvidiaBackend.__init__ leaves self.device None. The loader picked the precision default as `'fp16' if self.device == 'cuda' else 'fp32'` at line 897 and only detected the device at line 1029, so the FIRST load in a fresh engine process defaulted to fp32 on a CUDA card -- while a second load in the same process inherited 'cuda' from the first and quietly got fp16. The same model loaded at two precisions depending on load order. fp32 costs twice the VRAM of the dtype asked for, which on a 24 GB card is the whole headroom argument for quantizing in the first place: Qwen3.5-9B is carded load_in_4bit with an estimate of 7.795 GB and measured 14.403 GB, a 1.85x gap that is the fp32 tell. FlashAttention also refuses fp32 outright, so transformers warned twice at load, cast the inputs back to fp32 at inference, and the model answered every request with "FlashAttention only support fp16 and bf16 data type" -- and before that error was surfaced at all, with an empty completion. It had been unusable since 2026-09-30. Also resolve flash_attn by PRESENCE rather than truthiness: `config.get(k) or global` cannot express "off", so a card saying `flash_attention: false` fell through to the global default. Not the cause here (the global is false too), but the same class of bug one layer up. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The video path reserves a model's VRAM estimate for its whole load window so a concurrent load sees the card as spoken-for. The image path registered only a Tasks-page entry, so for as long as a load took, the card looked free. Measured 2026-10-28: Krea-2-RealDream held 18.8 GiB of a 23.56 GiB card for the 194 s of its load, ensure_vram_for granted a CrisperWhisper reservation against VRAM that was already gone, and every captioned scene of a concurrent compose answered "CUDA out of memory. Tried to allocate 86.00 MiB". Both ends had been fixed in isolation -- the STT path reserves, the worker releases -- and neither helped, because the model they were negotiating over never declared itself. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
place_at padded a mix up to the scene length with apad and let atrim end the stream. That handoff is a race. A bare apad loses it by generating silence for ever: ffmpeg never reaches EOF and spins at 100% CPU with the output half-written -- 4 hangs in 10 runs, and two orphans left on a dev host by test_compose_dialogue had burned 14h and 10h of CPU unnoticed. apad=whole_dur stops the hang and loses the same race the other way, emitting a track one clip short (3s for 4s) in 3 of 14 runs with no error anywhere. Neither -threads 1 nor -filter_threads 1 helps. A silent base input of exactly the target length, mixed under the placements with amix=duration=first, has no handoff to lose: 0 bad in 20 runs, one output size. The other apad sites feed no mix, but take whole_dur too since the length is known there as well. Also pass config/subfolder to the single-file component loader, the way the GGUF branch already does and for the same reason: left to itself from_single_file infers a base repo from the checkpoint's own metadata, so Krea-2-RealDream reached for krea/Krea-2-Raw and died on a repo nobody named. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The lip-sync engines were outside the ledger entirely. A render is gigabytes on the card for minutes, in a process the manager did not start, so the next model to load read the card as emptier than it is, took what it found, and one of the two died. They are models.json rows now -- video_models, backend "lipsync", capabilities lip_sync -- configurable on the models page like anything else, and a run goes through acquire_stt_backend, the same door LongCat and the STT workers use. The footprint is sampled during the run (these are third-party scripts that report nothing about themselves) and kept as a high-water mark, so the next reservation is measured. The registration is dropped when the process exits: one-shot work that stayed in the ledger would claim VRAM nobody holds. The bigger half is a defect that was never about lip sync. acquire_stt_backend POPS the instance pool, and _is_key_busy looks only at that pool -- so every model registered through it reads as permanently idle. LongCat mid-generation, a CrisperWhisper transcription, a lip-sync render: eviction was not choosing to take the card from busy work, it could not see the work was there. LongCat's own source notes this and works around it inside its releaser; nothing else did. So multi_model_manager grows external_busy(key), and _is_key_busy consults it. The existing eviction logic then does the right thing with no further change: the first pass skips a busy model, the second waits for the active one. An in-flight render is something to queue behind, not a candidate -- cleanup() waits for the process and never terminates it, because a half-finished render is worth nothing and the request that triggered the eviction is not more important than the one already running. Also: the Civitai redirect fix moved the seam two tests patched, so they stopped intercepting and started reaching the real site -- which is why they failed on HTTP 404 rather than on the 401/403 pages they assert about. Every request now leaves through one function, which is the thing to patch. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Two things that were only reachable from a python -c. The lip-sync engines had no management surface at all: no status, no install, no removal. They are not models.json entries on purpose -- each runs in its own interpreter as a subprocess, so a row in a model section would put them in the VRAM ledger as something the manager can load, which it cannot -- but that is a reason to give them their own surface, not none. So: a tab on the models page, three admin endpoints behind require_admin like every other model action, and an install that runs in the background while the page polls. The log is kept because when an install fails, WHERE it failed is the whole answer: a wheel that will not build reads nothing like a 401. "Rebuild venv" keeps the weights, since a dependency change does not alter six gigabytes. `component_models` could only name a repo. A community fine-tune is very often ONE file -- a retrained transformer with no repo around it, the base supplying the VAE, the text encoder and the scheduler -- and from_pretrained cannot open one, so such a component could only be named if somebody had published a converted directory tree for it. It now accepts a single-file or civitai: reference. And the Civitai resolver picked the LARGEST file of a version. Real Dream's Krea 2 V3 publishes the same weights three times: fp8 13.46 GB, int8 13.18 GB, nvfp4 8.12 GB. Largest-wins lands on fp8 by a 0.28 GB margin nobody chose, and could as easily have landed on nvfp4, which needs Blackwell and cannot load on an Ampere card at all. A reference may now name the file, and without one the choice is made among the builds this GPU can run, by precision rather than size. Two smaller ones found by using it. urllib replays headers across a redirect, so Civitai's signed R2 URL received a bearer token as well and answered 400 -- which looks exactly like a bad token, and meant adding a token BROKE downloads that had worked without one. curl strips it; now so does this. And the cache has its own root (CODERAI_CIVITAI_CACHE) instead of living under the HF cache, which is how a second GGUF directory grew inside one. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Four pins and one probe, each found by building it rather than reading about it. mmpose's RTMO head imports mmdet unconditionally although mmpose lists it as optional, so `import mmpose.apis` dies without it; mmdet then asserts mmcv < 2.2.0 at import; openmmlab publishes a cp311 mmcv 2.1.0 wheel on exactly one index, cu121/torch2.1. So the torch pin is 2.1.2 and it is dictated by mmcv, not by MuseTalk. mmengine imports pkg_resources to find mmpose's config directory, which setuptools dropped in 81, making setuptools<81 a runtime pin. chumpy, which mmpose depends on, has a setup.py that does `import pip` and cannot build in an isolated environment. Named per package rather than turning isolation off wholesale, which would let everything else build against a half-installed venv. The venv probe was wrong, not the install: it ran a bare interpreter, so face_detection -- vendored under musetalk/utils, not a package -- came back missing and a complete install was reported broken. It runs against the install root with the engine's own PYTHONPATH now, as a real run does, and asks for mmpose.apis rather than mmpose, since the package imports fine and the submodule is what fails. Also: uv's HTTP timeout is too short for hundred-megabyte wheels off third-party indexes, and the abort reads as a corrupt wheel rather than a timeout. And a per-run directory is swept after a day -- a failed run keeps its task file and frames, which is what you read to find out why, but not for ever. Verified live: MuseTalk v15 lip-synced a 2s 480x480 clip in 92s, 48 frames with the audio muxed, and the successful run removed its own directory. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
_reject_offloaded_quantized already catches a 4-bit model whose weights landed on the CPU -- that is what stopped Qwen3.5-9B answering every request with "". But it runs at the BOTTOM of the device_map ladder. So a full-GPU attempt that OOMs still walks every rung: 427 weight shards across GPU and host RAM, many minutes of it, before raising an error that was knowable when the ladder started. The line printed immediately above the fallback says as much -- "quantized kernels cannot read offloaded weights" -- and then falls through to the ladder anyway. The other half is that the attempt which failed was never retried honestly. It ran against whatever happened to be free on a shared card, which is a lottery; another model's residency is a reason to evict, not a reason to mis-place this one. Reproduced on the live server today: 7.2 GB free because an embedding model and a whisper-server held the rest, and nothing asked them to move. So a quantized model that fails full-GPU now evicts and tries once more, and refuses immediately if that still does not fit, naming the three things that help. Unquantized models keep the ladder: offload is legitimate for dense weights, only slower. This makes a failure faster and louder rather than slower and quieter, which is a behaviour change on a live path. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
LatentSync and MuseTalk have been selectable by name for a while, and `engine: "auto"` has ranked both ahead of wav2lip whenever there was a clip to drive -- wav2lip works at 96x96 and upscales, which is the soft mouth on a 1080p reel. Nothing ever installed them, so every selection ended at "neither on PATH nor installed", and the call that would have been made could not have worked either: LatentSync requires its checkpoint and UNet config as well as the three paths, and MuseTalk takes a YAML task list and writes into a result directory rather than taking paths at all. Coderai orchestrates what it serves, so it installs these. The inference source is vendored under third_party/ and pinned to a commit, for the reason LongCat's is: an image someone else pulls has to run without network access to a third party. The weights come from the Hub on first use, never during a render. Each engine gets its own venv on its own python, because both pin stacks the server cannot host. Upstream MuseTalk takes its BiSeNet face-parsing checkpoint off a Google Drive id and a pytorch.org URL; both come from a Hub mirror here, so the whole set has one provenance. _managed_engine_ready used to answer "an entrypoint and any .pt anywhere", which reads an interrupted download as a working install. It now asks the module that knows the file list, and the old heuristic keeps covering an engine an operator installed by hand. /v1/video/presenter/engines reports what an uninstalled engine is short of, so a client can tell "not yet" from "never here". Verified live: LatentSync installed from nothing and lip-synced a 2s 480x480 clip to real narration in 136s. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
Three CrisperWhisper models were configured and they stacked: each is a resident Whisper-large in its own process, so the card held two of them plus the server's text and embedding models until one OOMed inside the encoder's softmax. Nothing here was the model's fault. The reservation taken before a load was a hard-coded 3 GB -- roughly the fp16 weights, and about a third of what a verbatim word-timestamp pass peaks at, since that holds the encoder's attention for a whole 30s window. The worker now reports its own peak and the figure is recorded as a high-water mark, so the next reservation is measured rather than guessed. The eviction ladder had no way to reach a worker's VRAM: it lives in another process, and acquire_stt_backend pops the model pool, so the manager's busy signal cannot see a transcription either. There is an external releaser now -- idle workers go at once, a busy one is waited out to the drain budget and only then taken -- and asking for one model stops its idle siblings. A cold start was invisible while it lasted. Free VRAM comes from the driver, so a worker downloading its shards looks like room; live traffic loaded into it and the transcription OOMed on a card nobody over-committed on purpose. The load window holds a reservation now. whisper-server already evicts and knows when it is idle, but it sized itself at file size x 1.1, which counts the weights and not whisper.cpp's compute buffers. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
- 27 Oct, 2026 5 commits
-
-
Stefy Lanza (nextime / spora ) authored
The map takes 4bit / 8bit / 2bit or a .gguf path. Anything else was skipped without a word -- and one unusable entry empties the mapping, which takes the global load_in_4bit with it, so the component loads from the base repo at full precision while the operator believes it comes from the file they named. LTX-2.5 named a bf16 Gemma-4 12B for its text_encoder. The quantization config came back None, the 12B loaded unquantized, and every generation ended with the card full: 23.53 GiB of 23.56 GiB in use, unable to allocate 30 MiB, on a model whose own measured footprint is 14.45 GB. Four strategies and about an hour of 20-minute attempts, all of them really the same missing line. Now it says so, and names component_models -- the key that does what such a value was meant to do. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
video.py already knew the hazard and wrote it down: accelerate's device_map only dispatches components it LOADS, so one handed to from_pretrained keeps whatever device it is on with no offload hook, and the forward pass hits "index on cuda:0, weights on cpu". It acted on that only for pipeline-cache injections, while a GGUF component arrives exactly the same way. LTX-2.5 therefore failed under every strategy it tried -- auto, balanced, model and pure-disk, three restarts and half an hour each -- always with Expected all tensors to be on the same device, but got index is on cuda:0, different from other tensors on cpu and video_resident_experts, the flag that exists for precisely this, was never consulted because the gate in front of it was false. An explicit offload_strategy is still respected; only the strategies that let accelerate place components itself are overridden. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The LTX-2.5 key reconciliation shipped, the retry failed identically, and the repair had never run. It needs the component's config.json to know which names the class expects, and _component_dir looked only in the local snapshot -- which, for a component whose weights come from a GGUF, is precisely the subfolder nobody downloads. 20 GB of weights the file replaces are not worth fetching; a few hundred bytes of config are. Cache first, then one hf_hub_download of that component's config.json. Same lesson as a cache-only existence check on a rented pod: an answer of "not here" from a cache is not an answer about the repo. Co-Authored-By:
Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
The repair that fixed LTX-2.3's prompt-adaln names could only strip a suffix. LTX-2.5's GGUF needs a whole segment renamed: it calls the attention norms `q_norm` / `k_norm` where LTX2VideoTransformer3DModel expects `norm_q` / `norm_k`. 724 parameters, one to one, the same tensors -- and unreconciled they stay on the meta device, so a 37-minute load ends with Cannot copy out of meta tensor; no data! about weights the file was carrying all along. Measured by diffing the GGUF's 4349 tensors against the class's 4091 parameters: nothing was absent, 724 were simply spelled differently. Both directions, because nothing says which spelling the next exporter picks, and both repairs now run in one pass -- a checkpoint can need each for a different family of keys. The existing guard is unchanged: a key moves only when the class does not want its current name, does want the renamed one, and nothing already holds the target. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-
Stefy Lanza (nextime / spora ) authored
diffusers reports a missing single-file checkpoint as Invalid `pretrained_model_name_or_path` provided. Please set it to a valid URL. which sends you to check the repo id. The actual cause, measured on LTX-2.5, was an entry pointing into ~/.cache/huggingface on the HOST -- a path the container does not mount -- so the file existed everywhere except in the process trying to open it. Every other part of that model was ready. The path is probed before the load and named in the failure, with the reason it is usually absent. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
-