1. 29 Oct, 2026 4 commits
    • Stefy Lanza (nextime / spora )'s avatar
      fix the recovery path: a 4-bit module's dtype, and the card it kept · 9715d200
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      9715d200
    • Stefy Lanza (nextime / spora )'s avatar
      learn once whether a model fits on the card, instead of re-proving it · 6a7be148
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      6a7be148
    • Stefy Lanza (nextime / spora )'s avatar
      make the isolated speech engines actually start, and list their voices · d8ce96fa
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      d8ce96fa
    • Stefy Lanza (nextime / spora )'s avatar
      let a cache hit keep the card it was just sized for · 1f42d08c
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      1f42d08c
  2. 28 Oct, 2026 31 commits
    • Stefy Lanza (nextime / spora )'s avatar
      bring the text encoder back before using it again · df66aa56
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      df66aa56
    • Stefy Lanza (nextime / spora )'s avatar
      record that RunPod should be one provider of several, not the only one · 3f7f3a0f
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      3f7f3a0f
    • Stefy Lanza (nextime / spora )'s avatar
      make a per-model setting reachable by the name it was set on · 8105c605
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      8105c605
    • Stefy Lanza (nextime / spora )'s avatar
      C5: encode the prompt, then give the card back before denoising · 65844801
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      65844801
    • Stefy Lanza (nextime / spora )'s avatar
      size a model by the card that was asked for, and see every kind of load · dd9ba587
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      dd9ba587
    • Stefy Lanza (nextime / spora )'s avatar
      send a step callback only to a pipeline that takes one · b3aaec96
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      b3aaec96
    • Stefy Lanza (nextime / spora )'s avatar
      prefer VRAM by default, and let the suite run where production runs · 1ad16f15
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      1ad16f15
    • Stefy Lanza (nextime / spora )'s avatar
      A4: record what the image attention pin costs, so it is not re-investigated · 92a3d928
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      92a3d928
    • Stefy Lanza (nextime / spora )'s avatar
      let the model loader authenticate to the Hub, like the diarization worker always has · 46b4988e
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      46b4988e
    • Stefy Lanza (nextime / spora )'s avatar
      cap the container's memory, so a runaway engine dies instead of the box · 5b92f3fe
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      5b92f3fe
    • Stefy Lanza (nextime / spora )'s avatar
      C3: keep the soundtrack LTX generates, and stop reporting every file as silent · 45724c7a
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
      45724c7a
    • Stefy Lanza (nextime / spora )'s avatar
      C4: give Krea 2 the reference-image edit diffusers does not ship · 96a84b69
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      96a84b69
    • Stefy Lanza (nextime / spora )'s avatar
      v0.5.15 · e50f9e81
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      e50f9e81
    • Stefy Lanza (nextime / spora )'s avatar
      13, 13b and A4: progress a client can actually use, and a still that is not 70s · e3bb5c46
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      e3bb5c46
    • Stefy Lanza (nextime / spora )'s avatar
      v0.5.14 · 3a994952
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      3a994952
    • Stefy Lanza (nextime / spora )'s avatar
      green the whole suite: 17 failures, none of them a product bug · acf9ab5c
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      acf9ab5c
    • Stefy Lanza (nextime / spora )'s avatar
      v0.5.13 · 14825a0c
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      14825a0c
    • Stefy Lanza (nextime / spora )'s avatar
      14c + 14d: wrap text instead of shrinking it, and let a clip's sound come from its own file · 3ebc2bd5
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      3ebc2bd5
    • Stefy Lanza (nextime / spora )'s avatar
      read the music response as the type the endpoint returns · 6729e974
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      6729e974
    • Stefy Lanza (nextime / spora )'s avatar
      v0.5.12 · d6400f5d
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      d6400f5d
    • Stefy Lanza (nextime / spora )'s avatar
      make an ffmpeg we started unable to outlive us · 42d68ca1
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      42d68ca1
    • Stefy Lanza (nextime / spora )'s avatar
      let a model page set the precision it runs in · be14830d
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      be14830d
    • Stefy Lanza (nextime / spora )'s avatar
      choose the dtype default from a device that has been detected · 9646dd69
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      9646dd69
    • Stefy Lanza (nextime / spora )'s avatar
      let the eviction ladder see an image load while it runs · a3446118
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      a3446118
    • Stefy Lanza (nextime / spora )'s avatar
      build a track at its length instead of padding up to it · 143e85f1
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      143e85f1
    • Stefy Lanza (nextime / spora )'s avatar
      an out-of-band engine is a model: card it, track it, and queue behind it · 124941a4
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      124941a4
    • Stefy Lanza (nextime / spora )'s avatar
      give the lip-sync engines an operator, and let a component be one file · f035e581
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      f035e581
    • Stefy Lanza (nextime / spora )'s avatar
      make the MuseTalk install reach a working venv · e740d6e8
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      e740d6e8
    • Stefy Lanza (nextime / spora )'s avatar
      refuse a quantized model the card cannot hold, instead of proving it slowly · d94b144e
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      d94b144e
    • Stefy Lanza (nextime / spora )'s avatar
      install the engines the presenter has been preferring all along · 998d6d97
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      998d6d97
    • Stefy Lanza (nextime / spora )'s avatar
      reserve what a transcription costs, and let the card be taken back · f738df2c
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      f738df2c
  3. 27 Oct, 2026 5 commits
    • Stefy Lanza (nextime / spora )'s avatar
      a component_quantization value nobody understands must not vanish · 08df8843
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      08df8843
    • Stefy Lanza (nextime / spora )'s avatar
      a GGUF component is pre-built too, so placement must treat it as one · bcdd5ffb
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      bcdd5ffb
    • Stefy Lanza (nextime / spora )'s avatar
      the component probe asked a cache that had no reason to hold the answer · a2633773
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      a2633773
    • Stefy Lanza (nextime / spora )'s avatar
      reconcile the attention norms LTX-2.5 spells the other way round · b14e4ccd
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      b14e4ccd
    • Stefy Lanza (nextime / spora )'s avatar
      say "no such file" when a GGUF component is not on disk · 07235001
      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: 's avatarClaude Opus 5 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014T3rZnLhydX9QqUcfBbqLu
      07235001