1. 23 Jul, 2026 5 commits
  2. 22 Jul, 2026 20 commits
  3. 21 Jul, 2026 2 commits
    • Stefy Lanza (nextime / spora )'s avatar
      frontproxy: accept port-specific session_<port> cookie in telemetry auth · 8a9e0183
      Stefy Lanza (nextime / spora ) authored
      The session cookie is named session_<port> so two instances on one host don't
      clobber each other's cookie, but the lightweight front telemetry auth only
      checked the literal "session" name — 401'ing every front-served status call.
      Match the engine's get_current_user and accept any session / session_* cookie.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01EPLnsRpNBzWCHLgkXATqRz
      8a9e0183
    • Stefy Lanza (nextime / spora )'s avatar
      embeddings: multimodal (text+image) support + correct VRAM tracking/eviction · dd848607
      Stefy Lanza (nextime / spora ) authored
      Wire up real image embeddings and make embedding models first-class in the
      VRAM lifecycle.
      
      - api/embeddings.py: detect CLIP/SigLIP dual encoders and drive them through
        transformers get_text_features/get_image_features so text and images share
        one projected space (ST path kept for repos shipping a native recipe).
        request.image is now actually read (URL/data-URI/path/base64), vectors are
        appended after the text ones, and text-only models return a clear 400.
        Handle the transformers 5.x pooled-output return shape.
      - Wrap the loaded model in _EmbeddingModel (unpacks as (backend, model) but
        exposes cleanup()) and register it via add_model() + record_vram_delta() on
        the request path, so it is measured, LRU-tracked, and cleanly evicted like
        every other model type instead of leaking as a bare tuple.
      - admin: fix the model-load button for embeddings (was routed to the diffusers
        loader) to use _load_embedding_model, matching the request path.
      - admin: backfill used_vram_gb after a download completes, since the entry is
        saved before the weights exist on disk; factor the estimate into a shared
        _estimate_used_vram_gb helper. A freshly-downloaded CLIP/SigLIP entry now
        always carries a reasonable estimate so pre-load eviction sizes correctly.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01EPLnsRpNBzWCHLgkXATqRz
      dd848607
  4. 03 Jul, 2026 3 commits
    • Stefy Lanza (nextime / spora )'s avatar
      township: Run page invents characters/environments from scratch, phased · 15a0e81b
      Stefy Lanza (nextime / spora ) authored
      Two changes to the Run-page character/environment generation:
      
      1. Generate FROM SCRATCH via the text model instead of the built-in static pool.
         stage_characters/stage_environments always iterated FIGHTER_POOL/
         ENVIRONMENT_POOL and used their hardcoded (pre-fallback) prompts — the LLM was
         never invoked for a full run. New _invent_profiles() calls the text model
         (_autogen_profile_payload) to invent fresh profiles; the static pool is only a
         fallback when no text model is configured (with a clear warning).
      
      2. Phase the pipeline: invent ALL prompts first, then render ALL reference images,
         then train image LoRAs, then video LoRAs (prompts → images → image-LoRA →
         video-LoRA). New _render_profile_images() does the image phase from the saved
         prompts; the reuse/skip paths are unchanged. num_fighters/num_environments set
         how many to invent (default: the pool size).
      
      (CLI main() still uses the pool-based stage_characters/stage_environments; the
      web Run page is the phased-from-scratch path.)
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      15a0e81b
    • Stefy Lanza (nextime / spora )'s avatar
      lora-train: cross-engine GPU lock so nothing reloads mid-training (fix intermittent OOM) · 18c6a358
      Stefy Lanza (nextime / spora ) authored
      evict_cosited_siblings() frees the co-located sibling's VRAM ONCE at training
      start, but training runs for minutes as an in-engine background job that the front
      swap-gate can't cover (its POST returns a job_id immediately). So a concurrent LLM
      request reloaded the gguf text model mid-training and OOM'd the trainer (one
      fighter LoRA failed while others succeeded).
      
      Add codai/models/gpu_lock.py: a cross-engine GPU reservation. Training reserves the
      card for its whole duration — locally AND on co-located siblings via new
      /internal/gpu-reserve + /internal/gpu-release endpoints — and every ordinary
      model-load path (manager.request_model, video _load_video_pipeline, image
      _load_diffusers_pipeline) calls wait_until_free() first. A request that needs a
      load during training now BLOCKS until training releases, then loads and serves —
      the same queue-behind-the-owner behaviour the swap-gate gives request-level work,
      now extended to cover training. The training thread is exempt from its own
      reservation, so loading the base model never self-deadlocks; waits are bounded
      (900s) so a stuck reservation can't hang forever.
      
      Validated: sibling reservation blocks a loader thread until release; the reserving
      thread never waits on itself.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      18c6a358
    • Stefy Lanza (nextime / spora )'s avatar
      images: stop video's flash-attn backend leaking to image models (Z-Image attn_mask crash) · 0043eb2a
      Stefy Lanza (nextime / spora ) authored
      diffusers' Model.set_attention_backend() doesn't just set per-processor backends
      — it ALSO flips a process-wide active backend (attention_dispatch's
      _active_backend). The video path sets that to flash-attn for the Wan transformer;
      image and video share the nvidia-engine process, so the global stayed flash and
      leaked to the next image model. Z-Image's transformer sets no backend of its own
      (passes backend=None → uses the global) and its attention is masked, so it
      crashed with "`attn_mask` is not supported for flash-attn 2" → image/environment
      generation 400. reset_attention_backend() clears per-processor backends but NOT
      the global, so it didn't help.
      
      Fix: restore the diffusers global backend to the env default (native/SDPA)
      (a) before every image generation — bulletproof against a leaked flash backend —
      and (b) in the video pipeline teardown (_free_pipeline_vram), so it can't persist
      after a video pipe is freed. Masked image attention (SDPA) now always works; the
      video transformer keeps its own per-processor backend.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      0043eb2a
  5. 02 Jul, 2026 2 commits
    • Stefy Lanza (nextime / spora )'s avatar
      lora-train: evict co-located sibling engine's VRAM before training (fix OOM) · e300e9c4
      Stefy Lanza (nextime / spora ) authored
      LoRA training freed VRAM with unload_all_models(), which only unloads THIS
      engine's models. On the GGUF-isolation split the co-located gguf (text) engine
      kept its model resident (~7.4 GB), so fp32 training (~16 GB) + the sibling
      exceeded the 24 GB card → "CUDA out of memory. Tried to allocate 32 MiB … 26 MiB
      free … Process 226 has 7.36 GiB" — every fighter LoRA (dlaba, zigo, zlo, …)
      failed. Training also isn't covered by the front swap-gate, so nothing else
      cleared the sibling.
      
      Add multi_model_manager.evict_cosited_siblings(): invoke the registered
      cross-engine VRAM releasers (the cosite releaser posts wait=True, so it waits for
      a busy sibling to reach a safe point). Call it right after unload_all_models() in
      both training paths (image + video/Wan), so training gets the whole card.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      e300e9c4
    • Stefy Lanza (nextime / spora )'s avatar
      images: quantized models must not load in f32 (FlashAttention needs fp16/bf16) · b3014a3d
      Stefy Lanza (nextime / spora ) authored
      Z-Image-Turbo-unsloth-bnb-4bit (and any pre-quantized bnb/fp8/nf4/gptq/awq
      checkpoint, or a runtime-quantized model) dequantizes to a HALF compute dtype
      and its transformer uses FlashAttention, which only supports fp16/bf16. The
      per-model image loader defaults precision to f32, so such a model loaded in
      float32 and crashed with "FlashAttention only support fp16 and bf16 data type"
      (image/character generation → 400/500), besides wasting VRAM.
      
      When precision is left at the f32 default AND the model is quantized (name
      contains bnb/4bit/8bit/fp8/nf4/gptq/awq, or config sets load_in_4bit/8bit/
      component_quantization), load in bf16 instead. Non-quantized models keep the f32
      default. --no-ram already forced fp16, so it's unaffected.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      b3014a3d
  6. 01 Jul, 2026 4 commits
    • Stefy Lanza (nextime / spora )'s avatar
      pipeline-cache: reject corrupt-JSON caches (stop the empty-tokenizer death spiral) · 31793b05
      Stefy Lanza (nextime / spora ) authored
      A cache dir's completion marker only proves the save FINISHED, not that every
      file landed intact. A transient truncated write — repeatedly a 0-byte
      tokenizer/tokenizer_config.json — slipped into an otherwise "valid" cache and
      then threw JSONDecodeError on EVERY subsequent video load, knocking the pipeline
      off its fast path into the offload fallback ladder (balanced→sequential→disk),
      which churned for hours, leaked ~22 GB VRAM, and died on a meta-tensor error.
      
      Add _first_bad_json(dir): walk the (small) JSONs and flag the first that is
      empty or unparseable — the big weights are .safetensors and aren't scanned, so
      it's cheap. Wire it in on BOTH sides:
       - load: valid() and component_valid() now invalidate + return False when any
         cached JSON is corrupt, so a poisoned cache becomes a clean rebuild instead
         of a death spiral.
       - save: save()/save_component() verify the temp dir before committing, and
         mark_monolithic_complete() refuses to finalize a dir with a corrupt JSON —
         so a truncated write is never cached in the first place.
      Added invalidate_path(p) helper.
      
      Verified: _first_bad_json flags 0-byte and garbage JSONs, passes clean ones.
      The already-poisoned Wan2.2-VACE cache was deleted out-of-band to unblock.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      31793b05
    • Stefy Lanza (nextime / spora )'s avatar
      frontproxy: fix leaked GPU-swap slot on request cancellation (queued swap never fired) · df948c48
      Stefy Lanza (nextime / spora ) authored
      The GpuSwapGate.release() was async and awaited in the dispatch `finally`
      blocks. When a request was cancelled/interrupted mid-flight (client disconnect,
      an interrupted text generation), `await self._swap_release(...)` in the finally
      could itself be cancelled BEFORE it decremented the running counter — stranding
      a gate slot. With `running` stuck > 0, `_pump()` never ran, so a video request
      queued behind the interrupted text request was never granted: the GPU never
      swapped even though the text engine had gone idle (observed: video stuck ~37min
      while the text engine was idle for 11).
      
      Fix: make release()/_pump() SYNCHRONOUS and drop the asyncio.Lock. Every critical
      section is straight-line (no await between read and write), so under asyncio's
      single thread they're already atomic — and a synchronous release from a `finally`
      always completes even while the coroutine is being cancelled. acquire() keeps its
      one `await` (the event wait) with synchronous cancel-cleanup. All release call
      sites are now non-awaited. Added `[gpu-swap] queued/swapping` logging so the
      owner/queue/swap transitions are visible in debug.log.
      
      Validated: cancelling a text request whose slot a queued video is waiting behind
      now frees the slot and grants the video; a cancelled queued waiter leaks nothing.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      df948c48
    • Stefy Lanza (nextime / spora )'s avatar
      frontproxy: intelligent per-shared-GPU model-swap queue (batch, then swap) · c0a970b0
      Stefy Lanza (nextime / spora ) authored
      Builds on the cross-engine clean-swap eviction: instead of two engines on one
      shared card ever running forwards concurrently (→ VRAM contention → OOM →
      disk-thrash), the front now serializes model OWNERSHIP of a shared GPU while
      batching to avoid per-request thrash.
      
      New GpuSwapGate (frontproxy/reqqueue.py), one per shared-GPU group (keyed by the
      co-located engines' CODERAI_ENGINE_GPUS selector, created only when an engine has
      a sibling on its card):
      
        * A request for the model that currently OWNS the GPU runs immediately — a swap
          isn't needed (a lone stream never stalls). Concurrency stays capped downstream
          by the existing per-model FrontQueue.
        * A request for a DIFFERENT model queues. The owner keeps being served up to
          `cap` requests (server.gpu_swap_batch, default 10) while another model waits,
          then — once the owner is fully idle (never mid-request) — the GPU SWAPS to the
          waiting model (which evicts + loads), serves it, and round-robins BACK if the
          original has requests queued. No thrash (batch), no starvation (cap).
      
      Wired into all four dispatch paths (broker, broker-stream, direct stream with
      keepalive, direct non-stream) for every GPU-inference kind (text/image/video):
      acquire the swap slot before the per-model queue, release in the finalizer;
      cancelling a pending acquire (client disconnect) drops the waiter with no leak.
      The text-stream path emits keepalives while waiting out a swap so the client
      doesn't time out.
      
      Scheduler validated by async unit tests: cap engages at exactly N with a
      competitor waiting; a lone same-model stream runs unbounded; round-robin
      alternates; cancelled waiters leak no slot.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      c0a970b0
    • Stefy Lanza (nextime / spora )'s avatar
      manager: clean cross-engine VRAM swap (evict a busy sibling at its unit boundary) · c9791579
      Stefy Lanza (nextime / spora ) authored
      On the GGUF-isolation split, a torch (video/image) engine and a gguf (text)
      engine share one NVIDIA card. When one needed VRAM it asked the co-located
      sibling to release via /internal/evict-vram, but that only evicted the
      sibling's IDLE models and SKIPPED busy ones — so a text-model load would
      proceed into the VRAM an in-flight video clip still needed for its forward,
      and BOTH OOM'd. Recovery then laddered the video load down to disk offload
      and thrashed for ~1h.
      
      Give the cross-engine path the same wait-then-evict the local eviction
      already has: release_idle_vram(needed_gb, wait_for_busy, wait_timeout) first
      evicts idle models, then — only if still short — WAITS for each busy model to
      reach a safe idle point (between requests, e.g. between video clip parts) and
      evicts it. This converts contention into a CLEAN SWAP: the render's current
      unit finishes, its model is evicted, the sibling loads alone, and the render
      reloads + resumes on its next unit. Bounded by wait_timeout (180s) so two
      mutually-waiting busy engines can't deadlock — one gives up and falls back to
      its own CPU/disk offload.
      
      /internal/evict-vram now reads needed_gb + wait + wait_timeout from the body
      and forwards them; _cosite_vram_releaser sends wait=True with an HTTP timeout
      that exceeds the sibling's wait budget so the swap isn't cut short. Symmetric:
      both engines register the releaser at each other, so either direction swaps
      cleanly.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      c9791579
  7. 30 Jun, 2026 3 commits
    • Stefy Lanza (nextime / spora )'s avatar
      video: fix dual-expert OOM regression + harden pipeline VRAM teardown · e1ab02b1
      Stefy Lanza (nextime / spora ) authored
      Two fixes for the township video render failing with CUDA OOM and a
      ~17 GB "untracked teardown leak" that survived gc + empty_cache.
      
      1. Resident-experts regression. video_resident_experts now defaults to
         OFF. A dual-expert 14B model (Wan2.2-VACE-Fun: transformer +
         transformer_2, ~10 GB each at 4-bit) cannot hold both experts + text
         encoder + VAE + the activation peak in 24 GB; the resident load left
         transformer_2 on the CPU yet reported success, so the denoise loop
         (which needs both experts) OOM'd at step 0. 'model' CPU offload keeps
         only the active ~7 GB expert resident and swaps, so it fits. Also: when
         resident leaves ANY component off-GPU it is now treated as a failed
         load — the partial pipe is torn down and it falls through to model
         offload, instead of returning a half-loaded pipe that pins ~10 GB.
      
      2. Teardown leak. _free_pipeline_vram now breaks the references that
         outlived a plain component-null: reset any non-default attention
         backend, unload LoRA/PEFT adapters, run the pipe's own
         maybe_free_model_hooks()/reset_device_map() (frees accelerate offload
         hooks + their staging buffers), and drop the _coderai_* stamped attrs,
         before nulling components + gc + empty_cache.
      
      Verified live (image 0.1.33): 31 video units rendered, 0 OOM, 0 leak
      diagnostics, idle GPU back to ~6 GB.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      e1ab02b1
    • Stefy Lanza (nextime / spora )'s avatar
      township: make the tool web UI mobile-friendly · e63b008a
      Stefy Lanza (nextime / spora ) authored
      Add a @media (max-width:640px) block to the shared _CSS injected into every
      township page via _page(). On narrow screens: stack the .row/.row3/.modal .row2
      form grids to one column, wrap the nav bar, shrink the modal to fit a 320px
      viewport (min-width:0; width:94%), make the fixed-width 215/230px tile cards
      full-width, render inputs at 16px to stop iOS Safari focus-zoom, and give
      buttons roomier wrap-friendly tap targets. Desktop layout is unchanged.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      e63b008a
    • Stefy Lanza (nextime / spora )'s avatar
      packaging: build SageAttention into the OCI image (non-fatal, arch-gated) · c4b4b884
      Stefy Lanza (nextime / spora ) authored
      The diffusers video path uses SageAttention (INT8 attention) when available
      for faster Wan2.2 rendering. Like flash-attn it is CUDA-arch-sensitive, so
      it is built from source in the devel/builder stage against the just-installed
      torch, gated by BUILD_SAGEATTENTION (default 1) and SAGEATTENTION_ARCH
      (default 8.6 = RTX 3090). The build is non-fatal: on failure the image still
      works and the runtime attention-backend resolver falls back to flash/SDPA.
      
      build_oci_image.sh passes the three new args through (overridable via env:
      BUILD_SAGEATTENTION / SAGEATTENTION_REF / SAGEATTENTION_ARCH).
      
      Note: the fast update_oci_image.sh overlay is based on the runtime image (no
      nvcc), so it cannot build SageAttention — a full build_oci_image.sh is needed
      to bake it in.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01RdMufYvtTbtGDWsiZVoXce
      c4b4b884
  8. 29 Jun, 2026 1 commit