1. 24 Jul, 2026 17 commits
    • Stefy Lanza (nextime / spora )'s avatar
      oci: expose the NVIDIA GPU to Vulkan (NVIDIA_DRIVER_CAPABILITIES) for docker --nvidia · dbb0676d
      Stefy Lanza (nextime / spora ) authored
      The nvidia container runtime only injects the NVIDIA Vulkan ICD when the
      'graphics' driver capability is requested. --nvidia ran with the runtime default
      (compute,utility), so inside the container Vulkan could see only the AMD card
      (RADV) and llvmpipe — never the NVIDIA GPU. A Vulkan-only GGUF embedder
      (dinov2-embed, built with GGML_VULKAN) therefore could not run on the NVIDIA card
      at all; it was pinned to the AMD RX 580 or forced to CPU.
      
      Add `-e NVIDIA_DRIVER_CAPABILITIES=all` (honouring any caller-exported value) to
      the docker --nvidia args so the NVIDIA Vulkan ICD is injected and Vulkan
      enumerates the NVIDIA GPU. Enables running the GGUF dinov2 embedder on the 3090
      via Vulkan under the nvidia-gguf engine (device pinned via GGML_VK_VISIBLE_DEVICES
      / VK_ICD_FILENAMES; the build hardcodes ggml_backend_vk_init(0)). Takes effect on
      the next container (re)launch.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
      dbb0676d
    • Stefy Lanza (nextime / spora )'s avatar
      fix: Z-Image image-gen 500 — flash-attn-2 rejects attn_mask (shared-backend race) · 643fa155
      Stefy Lanza (nextime / spora ) authored
      POST /v1/images/generations for a masked image transformer (Z-Image) crashed
      with "`attn_mask` is not supported for flash-attn 2." → HTTP 500.
      
      The diffusers "active attention backend" is PROCESS-WIDE global mutable state
      (_AttentionBackendRegistry class attribute) shared by the image and video paths.
      The video path sets it to flash-attn-2. A transformer whose per-module backend
      is None reads that global at dispatch time; flash-attn-2 rejects attn_mask, so
      Z-Image (which uses caption masks) crashes. The previous fix reset the global to
      native before generating, but that reset is NOT atomic with the pipeline() call
      (it runs in a to_thread worker), so a concurrent/subsequent video generation
      races and flips the shared global back to flash before image attention dispatches.
      
      Fix: pin the image denoiser's per-module attention backend to native (SDPA,
      mask-supporting) via set_attention_backend("native"). The dispatcher passes
      processor._attention_backend explicitly, bypassing the shared global — so image
      attention is immune to the video path flipping it. Video is undisturbed (it sets
      its own explicit per-module 'flash'). Image pipelines were never intentionally on
      flash, so this is non-regressive. Global reset kept as a fallback.
      
      Verified: unsloth/Z-Image-Turbo-unsloth-bnb-4bit now returns 200 with an image;
      no "attn_mask is not supported" in the log.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
      643fa155
    • Stefy Lanza (nextime / spora )'s avatar
      fix: GME vision embeddings 500 on large images (KV context overflow) · e362e56c
      Stefy Lanza (nextime / spora ) authored
      POST /v1/embeddings to the GME-Qwen2-VL GGUF embedder returned HTTP 500 for
      large photos. The qwen2-vl vision tower turns a big image into >n_ctx image
      tokens (observed batches of 1012–2048), and mtmd's decode then fails —
      "decode: failed to find a memory slot for batch of size N" / "failed to eval
      chunk 1" — because the image tokens plus the chat-prompt frame exceed the
      model's KV context. Small images (few hundred tokens) were unaffected.
      
      mtmd's own image_max_tokens budget is NOT honoured by this llama.cpp build
      (2048-token batches slipped through), so add a hard, Python-side backstop:
      _resize_image_to_token_budget() downscales any oversize PIL image (aspect
      preserved, 28px patches) to at most n_ctx-128 tokens BEFORE it reaches mtmd,
      so a request can never overflow the context regardless of input size. The
      budget derives from the live n_ctx, so it self-adjusts to any model config.
      
      Pairs with raising the GME model's n_ctx (instance models.json) so a
      full-size image (~2048 tokens) fits comfortably; n_batch/n_ubatch follow
      n_ctx in the loader.
      
      Verified on the radeon Vulkan embedder: 336² / 896² / 1400² / 1680² images
      all return 200 (the 1400²/1680² are logged resizing to ~2400 tokens to fit
      the 2432 budget); previously the ≥1024-token cases 500'd.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
      e362e56c
    • Stefy Lanza (nextime / spora )'s avatar
      fix: front-driven thermal pause indicator no longer erased by engine poll · a7a09b13
      Stefy Lanza (nextime / spora ) authored
      On a global CPU cooldown the thermal supervisor already pauses ALL engines
      (want_pause = gpu_hot or cpu_hot, applied per-engine), and the logs confirm
      every engine gets a pause. But the Tasks page often showed only ONE engine
      as cooling while the others looked like they were still running.
      
      Cause: two threads write engine.cooling. The thermal loop sets it when it
      pauses an engine; the health poll loop overwrites it every tick with the
      engine's OWN self-reported cooldown (cooling=d.get("cooling")). An engine
      only self-reports cooling while sitting in its own wait_until_safe loop (it
      has an in-flight request). An engine the front paused while IDLE reports
      cooling=None, so the poll loop cleared the front's pause indicator — the UI
      then showed that engine as running mid-cooldown.
      
      Fix: while the front holds an engine paused (engine.therm_paused), pass the
      update_state sentinel (cooling=False, "don't touch") instead of the engine's
      self-report, so the front's indicator survives. Once the front resumes the
      engine, the engine's self-report flows through again as before.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
      a7a09b13
    • Stefy Lanza (nextime / spora )'s avatar
      fix: config-key shadow dropped mmproj on GGUF vision model reload · 1126bdbc
      Stefy Lanza (nextime / spora ) authored
      A GGUF vision model (e.g. Gemma-4-14B) served correct image descriptions
      on its FIRST load after a restart but hallucinated an identical answer for
      every image on every subsequent load — the image was silently flattened to
      a "[image_url content]" text placeholder and the model never saw pixels.
      
      Root cause was a config-key mismatch that self-polluted the in-memory
      config. On-demand loads arrive as a basename (Gemma-...gguf) while the real
      models.json entry is keyed by full path. record_vram_delta() resolved the
      write target via _config_for_model_key(), which — unlike _config_for_model()
      — did NOT fall back to basename/alias matching, so it returned {} and then
      persisted a NEW basename-keyed entry holding ONLY the measured_* fields (no
      mmproj, no n_ctx). On the next load _config_for_model()'s exact-match hit
      that stripped basename entry FIRST, before the basename loop that would have
      found the real full-path config, so mmproj was dropped, supports_vision went
      False, and the vision projector never loaded.
      
      Fix:
      - Add _resolve_config_key(): returns the actual self.config key for a model
        (exact -> alias -> basename), the single canonical key readers and writers
        must agree on.
      - Route _config_for_model() through it; give _config_for_model_key() the same
        basename/alias fallback so it can no longer return {} for a basename.
      - record_vram_delta()/_persist() now read and persist measured fields under
        the canonical key, merging into the real config instead of spawning a
        stripped shadow entry.
      
      Verified: after restart the 14B loads with "mmproj ... (vision enabled)" on
      every reload and three distinct test images produce three distinct, accurate
      descriptions; the measured-VRAM writeback now logs "(force_vram_update)"
      (real config resolved) instead of the old "(no used_vram_gb)" (empty config).
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
      1126bdbc
    • Stefy Lanza (nextime / spora )'s avatar
      front: rate limit now guarantees an idle gap AFTER each request + live-applies · 086c6543
      Stefy Lanza (nextime / spora ) authored
      Reworked engine_request_min_interval_ms from start-spacing to a proper
      post-completion gap: _rate_acquire holds a per-engine lock for the whole
      request, _rate_release frees it only `interval` ms AFTER completion (via
      loop.call_later, non-blocking) — so consecutive requests to the engine
      are ALWAYS separated by at least that idle GPU time regardless of request
      duration. Wired acquire/release into all 3 inference dispatch paths with
      release in every finally/early-return. _rate_acquire refreshes config on
      mtime change so a value saved in the web UI applies to the next request.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      086c6543
    • Stefy Lanza (nextime / spora )'s avatar
      admin: per-engine request rate limit configurable in the web UI · e1bd2277
      Stefy Lanza (nextime / spora ) authored
      Adds a "Rate limit (ms)" column to the Settings per-engine overrides
      table for engine_request_min_interval_ms (0 = no limit). Backend GET
      returns the map, POST saves it via the int-override sanitizer.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      e1bd2277
    • Stefy Lanza (nextime / spora )'s avatar
      front: configurable per-engine request rate throttle (min interval ms) · 19241dc1
      Stefy Lanza (nextime / spora ) authored
      New server.engine_request_min_interval_ms (engine name → ms, 0/unset =
      off). _rate_gate spaces inference dispatch STARTS to an engine by at
      least the interval, capping request rate and inserting idle time between
      GPU submissions — a stability lever for a marginal card (e.g. RX 580)
      that wedges under sustained back-to-back Vulkan compute. Wired into all
      three inference dispatch paths after the swap-gate; the request itself
      runs unthrottled, only the start cadence is gated.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      19241dc1
    • Stefy Lanza (nextime / spora )'s avatar
      config: per-engine env overrides (RADV driver tuning for Polaris hangs) · b6170681
      Stefy Lanza (nextime / spora ) authored
      New server.engine_env_overrides (engine name → {VAR: val}), merged into
      the engine's process env at spawn. Lets low-level driver knobs be set
      without hardcoding — set radeon → RADV_DEBUG=syncshaders to serialize
      RADV shader dispatch and test whether the Polaris compute-ring async
      race (ring comp_x.y.z timeout) is what wedges the RX 580 under sustained
      Vulkan compute.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      b6170681
    • Stefy Lanza (nextime / spora )'s avatar
      frontproxy: a wedged GPU can no longer hang the whole front · 09558635
      Stefy Lanza (nextime / spora ) authored
      At front startup _build_engines runs vulkaninfo to enumerate GPUs; on a
      wedged Polaris card that process blocks UNINTERRUPTIBLY (D-state), and
      subprocess.run(timeout=) can't kill a D-state child — so the front's
      uvicorn startup hung forever ("Waiting for application startup"),
      taking down the healthy NVIDIA engine too (502 everywhere).
      
      Add _call_with_timeout: runs the probe in a daemon thread and STOPS
      WAITING after a wall-clock timeout, leaving the un-killable call
      orphaned instead of blocking. Wrap vulkan_devices() (12s) so startup
      always completes, and _amd_stats() (6s) so a hung Radeon can't freeze
      the health/thermal poll threads that also monitor the healthy card.
      A wedged engine now degrades to "that engine down", not "everything down".
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      09558635
    • Stefy Lanza (nextime / spora )'s avatar
      admin: make per-engine GPU power lock configurable in the web UI · 49b66f0e
      Stefy Lanza (nextime / spora ) authored
      The per-engine concurrency-overrides table on the Settings page already
      exposes max_parallel_requests per engine; add a "GPU power lock" column
      (dpm level select) so dpm_force_performance_level_overrides is editable
      too. Backend: GET returns the map, POST saves it via a level-validating
      sanitizer. Takes effect on engine restart.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      49b66f0e
    • Stefy Lanza (nextime / spora )'s avatar
      packaging: rc.local helper to lock AMD GPU power state (eudev bind unsupported) · 91ee14d0
      Stefy Lanza (nextime / spora ) authored
      Devuan's udev rejects the 'bind' trigger action, so the udev-rule path
      can't reliably apply the DPM lock at boot. sysvinit's /etc/rc.local runs
      race-free after amdgpu binds — this root-owned helper, called from
      rc.local, sets power_dpm_force_performance_level=high on every AMD card
      by PCI vendor id.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      91ee14d0
    • Stefy Lanza (nextime / spora )'s avatar
      packaging: fix amdgpu DPM udev rule — fire on bind via RUN, not add ATTR · f1f33a10
      Stefy Lanza (nextime / spora ) authored
      An ATTR{}= assignment on the add event races the amdgpu probe (the
      power_dpm attribute doesn't exist yet), so it silently no-ops at boot
      (observed: level stayed 'auto'). Fire on the bind action — driver bound
      and sysfs attrs created — and write via RUN with %p devpath.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      f1f33a10
    • Stefy Lanza (nextime / spora )'s avatar
      packaging: udev rule to persist AMD GPU high power state across boots · 2b46b02d
      Stefy Lanza (nextime / spora ) authored
      Devuan (sysvinit/OpenRC, no systemd) — a udev rule is the init-agnostic
      way to make power_dpm_force_performance_level=high stick. Matches by
      DRIVER==amdgpu so it survives DRM card renumbering across GPU resets.
      Complements the in-container best-effort apply (unprivileged, can't
      write root sysfs) shipped in 0.1.53.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      2b46b02d
    • Stefy Lanza (nextime / spora )'s avatar
      config: per-engine AMD GPU power-state lock (Polaris compute-hang mitigation) · 20b55838
      Stefy Lanza (nextime / spora ) authored
      New server.dpm_force_performance_level_overrides (engine name → level,
      e.g. {"radeon":"high"}). At engine startup the engine writes the level
      to every amdgpu card's power_dpm_force_performance_level — locking a
      Polaris/GCN card to fixed top clocks avoids the DPM power-state
      transitions that hang these cards under sustained Vulkan compute
      (current default is 'auto', the hang-prone mode). Card is matched by PCI
      vendor id (0x1002), robust to DRM card renumbering across resets.
      Best-effort: an unprivileged container logs the exact host command on
      PermissionError.
      
      Pairs with two config-only stability levers applied for the radeon:
      n_ctx reduction (qwen3 1024→512, gme 1536→1024) to keep all three
      embedders in real VRAM with headroom (no GTT-over-PCIe spill during
      compute — another Polaris hang trigger), and max_parallel_requests
      override {"radeon":1} to serialize Vulkan submissions.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      20b55838
    • Stefy Lanza (nextime / spora )'s avatar
      vision: transcode AVIF/WebP images to PNG for the llama.cpp chat handler · 0c69e4bc
      Stefy Lanza (nextime / spora ) authored
      A vision-capable GGUF model (gemma-4-26B + mmproj) replied "none" to
      real-estate photos because the client sends AVIF, and llama.cpp's mtmd
      chat handler decodes data-URIs with stb_image, which has no AVIF/WebP
      support — the model silently received no image. _normalize_vision_content
      now transcodes any non-stb format to PNG via PIL (which has libavif) before
      handing it to the handler. The radeon GME embedder was unaffected (it
      already decodes via PIL). Text-only models (gemma-2-9b, no mmproj) are
      untouched — the image is still flattened, since they have no vision tower.
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      0c69e4bc
    • Stefy Lanza (nextime / spora )'s avatar
      thermal: filter impossible GPU temps at the source (gpu_detect + read chokepoint) · 9ff0f709
      Stefy Lanza (nextime / spora ) authored
      The 0.1.50 guard only covered _read_gpu_temp_uncached's FIRST probe path;
      its rocm/psutil fallbacks and gpu_eval() (which reads engine_gpu_stats
      directly) still saw the raw 511°C and cooling-waited a healthy card
      forever. Filter at the real source — the amdgpu sysfs temp1_input read
      in gpu_detect — so temp is None for any ≥150°C reading and NO downstream
      reader can act on it; plus a final chokepoint in read_gpu_temp().
      Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
      Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
      9ff0f709
  2. 23 Jul, 2026 9 commits
  3. 22 Jul, 2026 14 commits