load the vision tower a checkpoint asks for, and refuse the pictures we cannot read

A model listed as able to read images has to read them, or say it cannot.

`Qwen3.5-9B` was listed with `image_to_text` and answered every photograph
with "a blank white image with no visible content" — nine real pictures of
one video, indexed as blank. Two causes, one symptom:

  AutoModelForCausalLM        -> Qwen3_5ForCausalLM            (text only)
  AutoModelForImageTextToText -> Qwen3_5ForConditionalGeneration

The engine asked for the first, so the tower was never built, and with no
projector the chat path flattened the picture to the literal text
"[image_url content]". The model was answering the prompt it was given.

Now the class comes from the checkpoint's own config, every load path in the
CUDA backend goes through that resolver, a vision model also loads the
processor that makes a picture promptable, and an image request takes its own
generation path (no KV prefix reuse — image tokens come from the pixels).
Where a model genuinely cannot see, the request is a 400 rather than a
confident wrong answer, and a GGUF with no mmproj stops advertising the
capability it cannot serve.

Two things found while measuring the image path on a live fleet:

* every image load printed "Proactive eviction skipped: name 'request' is not
  defined". `_load_diffusers_pipeline` has no `request`; the NameError was
  swallowed as a skip, so the minimum-eviction block never ran and each load
  fell through to the blunt eviction that unloads models which would have
  fitted alongside.
* `Z-Image-Turbo`, distilled for 8 steps at CFG 1, was served at 30 steps and
  CFG 7.5 because its card carried no `defaults` — four times the work for an
  over-guided picture, ~72 s a still. A card that says nothing now falls back
  to what the model's own name says before the generic floor.
Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T3rZozi1Ehk96NG1WesgC
parent 8274ff3e
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.5.16" __version__ = "0.5.17"
__tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference." __tagline__ = "Complete orchestration, distribution and escalation of remotizable advanced inference."
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
......
...@@ -647,6 +647,12 @@ def _load_diffusers_pipeline(model_name: str, global_args, model_config: dict = ...@@ -647,6 +647,12 @@ def _load_diffusers_pipeline(model_name: str, global_args, model_config: dict =
_mc = model_config or {} _mc = model_config or {}
# The id the CALLER asked for, kept before `model_name` is rewritten to the
# checkpoint path below. Sizing needs it: two cards can share one
# checkpoint with different quantization and offload, and the one that was
# asked for is the one whose footprint applies.
_requested_id = model_name
# A multi-config sibling is registered under its alias, so `model_name` may # A multi-config sibling is registered under its alias, so `model_name` may
# BE that alias rather than something from_pretrained can open. The config # BE that alias rather than something from_pretrained can open. The config
# carries the checkpoint it is a config of; prefer it when present. For a # carries the checkpoint it is a config of; prefer it when present. For a
...@@ -845,9 +851,15 @@ def _load_diffusers_pipeline(model_name: str, global_args, model_config: dict = ...@@ -845,9 +851,15 @@ def _load_diffusers_pipeline(model_name: str, global_args, model_config: dict =
# requested_id: same-path siblings (one checkpoint, two cards # requested_id: same-path siblings (one checkpoint, two cards
# with different settings) must be sized by the card that was # with different settings) must be sized by the card that was
# ASKED for, not by whichever one registered the path. # ASKED for, not by whichever one registered the path.
# `request` does not exist in this function. Reading it raised
# NameError on EVERY image load, and the except below swallowed
# it as "Proactive eviction skipped: name 'request' is not
# defined" -- so the whole minimum-eviction block never ran and
# every image load fell through to the blunt eviction further
# down, which unloads a model that would have fitted alongside.
_need_gb = _mmm._get_model_used_vram_gb( _need_gb = _mmm._get_model_used_vram_gb(
_key or model_name, model_name, _key or model_name, model_name,
requested_id=getattr(request, 'model', None)) requested_id=_requested_id)
if _need_gb > 0: if _need_gb > 0:
if _free_gb < _need_gb: if _free_gb < _need_gb:
print(f"Image model needs {_need_gb:.1f} GB, {_free_gb:.1f} GB free " print(f"Image model needs {_need_gb:.1f} GB, {_free_gb:.1f} GB free "
...@@ -1558,13 +1570,36 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request= ...@@ -1558,13 +1570,36 @@ async def _generate_with_diffusers(pipeline, request, global_args, http_request=
_d_cfg = _dflt.get("guidance_scale") _d_cfg = _dflt.get("guidance_scale")
if _d_cfg is None: if _d_cfg is None:
_d_cfg = _dflt.get("cfg_scale") _d_cfg = _dflt.get("cfg_scale")
# A distilled checkpoint whose card says nothing. 30 steps at CFG 7.5 is
# the wrong answer for a model distilled for 8 at CFG 1 -- four times the
# work AND an over-guided, burnt-looking picture. Below the card's own
# defaults and below a fused distill LoRA, because both know more; above
# the generic fallback, because the model's own name knows more than that.
_distilled = None
if not _d_steps and _accel_steps is None:
try:
from codai.models.acceleration import distilled_defaults_for
_distilled = distilled_defaults_for(
str((model_config or {}).get('path') or '')
or str(getattr(request, 'model', '') or ''))
except Exception:
_distilled = None
if _distilled and not (request.steps and request.guidance_scale):
print(f" '{_distilled['token']}' in the model id: this checkpoint is "
f"distilled, so {_distilled['steps']} steps at CFG "
f"{_distilled['guidance_scale']} (its card carries no defaults; "
f"the generic fallback would be 30 at CFG 7.5)")
num_steps = request.steps if request.steps else ( num_steps = request.steps if request.steps else (
_accel_steps if _accel_steps else _accel_steps if _accel_steps else
(int(_d_steps) if _d_steps else (30 if quality == "standard" else 50))) (int(_d_steps) if _d_steps else
(int(_distilled["steps"]) if _distilled else
(30 if quality == "standard" else 50))))
cfg_scale = request.guidance_scale if request.guidance_scale else ( cfg_scale = request.guidance_scale if request.guidance_scale else (
_accel_cfg if _accel_cfg is not None else _accel_cfg if _accel_cfg is not None else
(float(_d_cfg) if _d_cfg is not None else (float(_d_cfg) if _d_cfg is not None else
(getattr(global_args, 'image_cfg_scale', 7.5) if quality == "standard" else 9.0)) (float(_distilled["guidance_scale"]) if _distilled else
(getattr(global_args, 'image_cfg_scale', 7.5) if quality == "standard" else 9.0)))
) )
# The sampler: what the caller asked for, else what the card recommends. # The sampler: what the caller asked for, else what the card recommends.
# A bad name from the CALLER is a 400 -- they can fix it -- while a bad one # A bad name from the CALLER is a 400 -- they can fix it -- while a bad one
......
...@@ -1620,6 +1620,31 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request ...@@ -1620,6 +1620,31 @@ async def chat_completions(request: ChatCompletionRequest, http_request: Request
# flattened to a text placeholder, so the multimodal handler can see it. # flattened to a text placeholder, so the multimodal handler can see it.
_vision_ok = bool(getattr(getattr(current_manager, 'backend', None), 'supports_vision', False)) _vision_ok = bool(getattr(getattr(current_manager, 'backend', None), 'supports_vision', False))
# A picture sent to a model that cannot see it must not be answered.
#
# Measured 2026-10-29: Qwen3.5-9B is listed with `image_to_text`, was
# loaded without its vision tower, and answered every photo with "a blank
# white image with no visible content" -- because the image had been
# replaced, below, by the literal text "[image_url content]". The model was
# answering the prompt it got. A caller cannot tell that answer from a real
# one, so it indexed nine photographs as blank. Refusing is the only honest
# outcome: a client can fall back to a model that can see, and cannot fall
# back from a confident wrong answer.
if not _vision_ok:
for _m in (request.messages or []):
_c = getattr(_m, 'content', None)
if not isinstance(_c, list):
continue
if any(isinstance(_it, dict) and _it.get('type') in ('image_url', 'input_image')
for _it in _c):
_release_instance()
raise HTTPException(status_code=400, detail=(
f"Model '{requested_model}' cannot read images: it is served "
f"without a vision tower (an HF checkpoint with no "
f"vision_config or processor, or a GGUF with no mmproj). "
f"Send the image to a model whose capabilities list "
f"image_to_text, or configure an mmproj for this one."))
# Inject system prompt if --system-prompt flag was provided # Inject system prompt if --system-prompt flag was provided
messages = request.messages messages = request.messages
global_system_prompt = get_global_system_prompt() global_system_prompt = get_global_system_prompt()
......
This diff is collapsed.
This diff is collapsed.
...@@ -423,3 +423,60 @@ def accel_call_defaults(accel: Optional[dict]) -> dict: ...@@ -423,3 +423,60 @@ def accel_call_defaults(accel: Optional[dict]) -> dict:
if accel.get("guidance_scale") is not None: if accel.get("guidance_scale") is not None:
out["guidance_scale"] = float(accel["guidance_scale"]) out["guidance_scale"] = float(accel["guidance_scale"])
return out return out
# A distilled checkpoint says so in its own name. These are the families whose
# whole point is a low step count, with the step/guidance pair each one was
# distilled for. The numbers are the conservative end of each family's
# published range: an 8-step model given 8 steps is right, and a 4-step model
# given 8 is slower than it needs to be but still correct, while the generic
# 30-step default is wrong for both.
#
# `boundary` marks the tokens that are words rather than fragments, so
# "turbovision" or "lcmx" cannot match on a substring.
_DISTILLED_NAMES = (
# (token, boundary, steps, guidance_scale)
("turbo", True, 8, 1.0),
("lightning", False, 8, 1.0),
("lightx2v", False, 8, 1.0),
("hyper-sd", False, 8, 1.0),
("hyper_sd", False, 8, 1.0),
("schnell", False, 4, 1.0),
("lcm", True, 6, 1.5),
("tcd", True, 8, 1.0),
("dmd2", False, 4, 1.0),
("distill", False, 8, 1.0),
("sdxs", True, 2, 1.0),
)
def distilled_defaults_for(model_id: str) -> Optional[dict]:
"""``{steps, guidance_scale, token}`` when this id names a distilled model.
The last resort before the generic 30-step / CFG-7.5 fallback, and only
reached when the operator's card carries neither ``defaults`` nor an
``acceleration`` block.
Measured on zeiss, 2026-10-29: ``Z-Image-Turbo`` -- a model distilled for 8
steps at CFG 1 -- was served at 30 steps and CFG 7.5 because its card had
no defaults, which is roughly four times the work for a picture that also
comes out over-guided. Both halves matter: a distilled model at CFG 7.5
looks burnt, so this is not only a speed fix.
A card's own `defaults` still win, because an operator who wrote a number
knows something this does not.
"""
n = (model_id or "").lower().replace("\\", "/").rsplit("/", 1)[-1]
if not n:
return None
for token, boundary, steps, cfg in _DISTILLED_NAMES:
at = n.find(token)
while at != -1:
if not boundary:
return {"steps": steps, "guidance_scale": cfg, "token": token}
before = n[at - 1] if at > 0 else ""
after = n[at + len(token):at + len(token) + 1]
if not before.isalnum() and not after.isalnum():
return {"steps": steps, "guidance_scale": cfg, "token": token}
at = n.find(token, at + 1)
return None
...@@ -6860,6 +6860,21 @@ class MultiModelManager: ...@@ -6860,6 +6860,21 @@ class MultiModelManager:
caps = ModelCapabilities() # drop the wrong guess caps = ModelCapabilities() # drop the wrong guess
setattr(caps, TYPE_MIN_CAP[model_type], True) setattr(caps, TYPE_MIN_CAP[model_type], True)
resolved_type = model_type or (caps.to_list()[0].split("_")[0] if caps.to_list() else "text") resolved_type = model_type or (caps.to_list()[0].split("_")[0] if caps.to_list() else "text")
# A GGUF text model with no projector cannot read an image, whatever
# its card says. llama.cpp needs an mmproj file to turn pixels into
# embeddings; without one the request reaches the model as a text
# placeholder and the answer describes nothing. Advertising
# `image_to_text` there sends clients to a model that will now
# (correctly) refuse them, so the listing has to agree with the
# runtime. Only local GGUF entries are judged here: an HF checkpoint
# loads its own vision tower when it has one, and a remote/pod-served
# model is the remote's business.
if caps.image_to_text and resolved_type == "text":
_p = str(meta.get("path") or meta.get("model_path") or "")
_backend = str(meta.get("backend") or "").lower()
if (_p.lower().endswith(".gguf") and not meta.get("mmproj")
and _backend not in ("vllm", "runpod", "remote", "colibri")):
caps.image_to_text = False
# A video model carries its geometry: the frame rate it was trained at, # A video model carries its geometry: the frame rate it was trained at,
# the frame counts its VAE can decode, how much one render holds and # the frame counts its VAE can decode, how much one render holds and
# whether it continues natively. Without it every client plans video by # whether it continues natively. Without it every client plans video by
......
...@@ -33,6 +33,7 @@ reference; these are the deep dives. ...@@ -33,6 +33,7 @@ reference; these are the deep dives.
| [zimage-lora-training.md](zimage-lora-training.md) | LoRA training for image models | | [zimage-lora-training.md](zimage-lora-training.md) | LoRA training for image models |
| [text-to-music.md](text-to-music.md) | `POST /v1/audio/generate` with ACE-Step: style prompt + sung lyrics → a 48 kHz stereo track, the python-3.12 worker it needs, and the two image variants (`--with-acestep`) | | [text-to-music.md](text-to-music.md) | `POST /v1/audio/generate` with ACE-Step: style prompt + sung lyrics → a 48 kHz stereo track, the python-3.12 worker it needs, and the two image variants (`--with-acestep`) |
| [dtype-auto-selection.md](dtype-auto-selection.md) | How model dtype is chosen automatically | | [dtype-auto-selection.md](dtype-auto-selection.md) | How model dtype is chosen automatically |
| [vision-chat.md](vision-chat.md) | Images in a chat request: loading the vision tower the checkpoint's config asks for, the processor that makes a picture promptable, and why a model that cannot see refuses instead of answering blind |
| [memory-accounting.md](memory-accounting.md) | Where `measured_vram_gb` comes from: walking the loaded model (quantization needs no factor), costing the KV cache from the real GQA/sliding-window geometry, refusing a measurement from a failed load, the PSS-based host-RAM cap, idle unload, and `GET /v1/models/history` | | [memory-accounting.md](memory-accounting.md) | Where `measured_vram_gb` comes from: walking the loaded model (quantization needs no factor), costing the KV cache from the real GQA/sliding-window geometry, refusing a measurement from a failed load, the PSS-based host-RAM cap, idle unload, and `GET /v1/models/history` |
| [model-defaults-and-sources.md](model-defaults-and-sources.md) | A model's own `defaults` (steps/guidance/sampler/modes/licence), the sampler names and the flow-matching boundary, and where weights come from: repo, single file, `org/repo:file` or `civitai:<versionId>` | | [model-defaults-and-sources.md](model-defaults-and-sources.md) | A model's own `defaults` (steps/guidance/sampler/modes/licence), the sampler names and the flow-matching boundary, and where weights come from: repo, single file, `org/repo:file` or `civitai:<versionId>` |
......
...@@ -36,7 +36,36 @@ Precedence, highest first: ...@@ -36,7 +36,36 @@ Precedence, highest first:
2. a **fused** distillation preset detected on the pipeline (`acceleration`), 2. a **fused** distillation preset detected on the pipeline (`acceleration`),
because it knows the LoRA actually baked in; because it knows the LoRA actually baked in;
3. `defaults` from the model entry; 3. `defaults` from the model entry;
4. the generic floor (30 steps / CFG 7.5 for images, 25 / 7.5 for video). 4. **what the model's own name says**, when nothing above answered (images);
5. the generic floor (30 steps / CFG 7.5 for images, 25 / 7.5 for video).
### 4: the name, when the card says nothing
A card with no `defaults` and no `acceleration` block used to fall straight to
the floor. Measured on a production fleet, 2026-10-29: `Z-Image-Turbo` — a
checkpoint distilled for 8 steps at CFG 1 — was served at 30 steps and CFG
7.5, about four times the work for a picture that also comes out over-guided,
because a distilled model given real classifier-free guidance looks burnt.
Roughly 72 s per 768×1344 still, for a model whose whole point is to take
seconds.
So when nothing better is available, the id itself is read:
| in the id | steps | CFG |
|---|---|---|
| `turbo`, `lightning`, `lightx2v`, `hyper-sd`, `tcd`, `distill` | 8 | 1.0 |
| `lcm` | 6 | 1.5 |
| `schnell`, `dmd2` | 4 | 1.0 |
| `sdxs` | 2 | 1.0 |
Only the last path segment is read, so an org named `turbo-labs` proves
nothing, and the word must stand on its own: `turbovision-xl` and `lcmx` do
not match. The choice is logged with the number it replaced, because an
unexplained step count is indistinguishable from a bug when a picture comes
out wrong.
This is a floor, not a recommendation: writing `defaults` on the card is still
the right thing to do, and it wins.
`/v1/models` publishes `defaults`, `modes` and `license` so a UI can show them `/v1/models` publishes `defaults`, `modes` and `license` so a UI can show them
instead of inventing them. `modes` is read from `defaults.modes`, a top-level instead of inventing them. `modes` is read from `defaults.modes`, a top-level
......
# Images in a chat request
A model that is listed as able to read images has to read them. If it cannot,
the request is refused — it is never answered blind.
## What used to happen
`/v1/models` listed `Qwen3.5-9B` with `["text_generation", "image_to_text"]`.
A chat whose user turn carried an `image_url` part came back, for every
picture, with a description of nothing:
```json
{"shows": "a blank white image with no visible content"}
```
Nine photographs of one video were indexed that way. They were real pictures —
the footage scorer rated them 0.53–0.59 — and the model was not hallucinating.
It was answering the prompt it had been given, which was the literal text
`[image_url content]`.
Two causes, measured on transformers 5.12 against `Qwen/Qwen3.5-9B`, whose
`config.json` carries a `vision_config` and an `image_token_id`:
| auto class | resolves to | holds the vision tower |
|---|---|---|
| `AutoModelForCausalLM` | `Qwen3_5ForCausalLM` | no |
| `AutoModelForImageTextToText` | `Qwen3_5ForConditionalGeneration` | yes |
The engine asked for the first one, so the tower was never built and its
weights were never read. And with no projector loaded, the chat path flattened
multipart content to a text placeholder, because that is all it could do with
a picture.
## What happens now
**The class comes from the checkpoint's config, not from its name.**
`codai/backends/hf_vision.py` reads the config: a nested `vision_config` or an
`image_token_id`, plus an `AutoModelForImageTextToText` mapping in the
installed transformers, means the model is loaded with the class that holds
its tower. Every `from_pretrained` in the CUDA backend goes through that
resolver, and any uncertainty — unreadable config, unknown family, missing
mapping — falls back to the text class, so a model that is not a vision model
loads exactly as it did before.
Deciding from the name does not work in either direction: `Qwen3.5-9B` carries
no "vl" or "vision" and is a vision model, while plenty of ids containing
"vision" are not image-text-to-text at all.
**A vision model also loads its processor.** The `AutoProcessor` — not the
tokenizer — owns the image preprocessing and knows how many placeholder tokens
this family expands one picture into. `supports_vision` is set only when both
halves are present: a class that holds a tower *and* a processor that can
preprocess for it. A tokenizer-shaped "processor" with no image processor does
not count; transformers returns one for some text checkpoints and it would
pass every duck-type check until the first picture arrived.
**An image request takes its own generation path.** The prompt is rendered by
the processor's chat template and then fed through `processor(text=…,
images=[…])`, which yields `pixel_values` and the grid metadata the tower
needs. Images travel as a separate list in the order they appear in the turn,
because that order is what binds a picture to its placeholder.
That path deliberately does not use the KV prefix cache. Image tokens are
expanded from the pixels themselves, so a prefix cached from another request
would be reused against the wrong picture.
**A picture sent to a model that cannot see is a 400.**
```
Model 'X' cannot read images: it is served without a vision tower (an HF
checkpoint with no vision_config or processor, or a GGUF with no mmproj).
Send the image to a model whose capabilities list image_to_text, or
configure an mmproj for this one.
```
A client can fall back from a refusal. It cannot fall back from a confident
wrong answer, which is why this is an error and not a warning.
**The listing agrees with the runtime.** A local GGUF text entry with no
`mmproj` no longer advertises `image_to_text`: llama.cpp needs that projector
file to turn pixels into embeddings, and without it the request would now be
refused. HF checkpoints keep the capability — they load their own tower when
they have one — and a remote or pod-served model is the remote's business.
## Configuring vision
* **HF checkpoint** (`text_models`, `vision_models`): nothing to set. If the
config has a vision tower and the installed transformers knows the family,
it is loaded and used.
* **GGUF**: set `mmproj` on the entry to the projector file. Without it the
model is text-only, and is listed as text-only.
* **Remote / pod-served** (vLLM, RunPod): vision is the remote's, and is
reported as supported.
## Checking it
```bash
curl -s $HOST/v1/models | jq -r '.data[] | select(.capabilities[]? == "image_to_text") | .id'
```
Then send one picture to one of those ids, and the same picture to a text
model. The first answers about the image; the second answers `400`. Neither
describes a blank page.
# CoderAI - OpenAI-compatible API server
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""A turbo model must not be served at 30 steps and CFG 7.5.
Measured on zeiss, 2026-10-29. The card for `Z-Image-Turbo` carried no
``defaults`` and no ``acceleration`` block, so a request that did not name a
step count got the generic fallback: 30 steps at CFG 7.5, against a checkpoint
distilled for 8 steps at CFG 1. That is about four times the work, and the
picture also comes out over-guided -- a distilled model at CFG 7.5 looks
burnt. The client's own report said "a turbo, few-step model should take
seconds" and measured ~72 s per 768x1344 still.
The order of precedence is the point of these tests: the request wins, then a
fused distill LoRA, then the operator's card, then the model's name, and only
then the generic number. Each step down is a weaker claim about the same
model, and nothing weaker may overrule anything stronger.
"""
import ast
import io
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from codai.models.acceleration import distilled_defaults_for
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_SRC = io.open(os.path.join(_ROOT, "codai/api/images.py"), encoding="utf-8").read()
def test_the_distilled_families_are_recognised():
assert distilled_defaults_for("Tongyi-MAI/Z-Image-Turbo")["steps"] == 8
assert distilled_defaults_for("unsloth/Krea-2-Turbo")["steps"] == 8
assert distilled_defaults_for("black-forest-labs/FLUX.1-schnell")["steps"] == 4
assert distilled_defaults_for("ByteDance/SDXL-Lightning")["steps"] == 8
lcm = distilled_defaults_for("latent-consistency/lcm-lora-sdxl")
assert (lcm["steps"], lcm["guidance_scale"]) == (6, 1.5), (
"LCM is the one family that wants guidance above 1")
def test_a_distilled_model_is_never_given_real_cfg():
for name in ("Tongyi-MAI/Z-Image-Turbo", "stabilityai/sdxl-turbo",
"black-forest-labs/FLUX.1-schnell", "ByteDance/Hyper-SD"):
got = distilled_defaults_for(name)
assert got is not None, name
assert got["guidance_scale"] <= 1.5, (
f"{name} would be served with classifier-free guidance it was "
f"distilled to do without")
def test_a_non_distilled_model_is_left_alone():
"""A false positive is the dangerous direction: it would serve a full
model at 8 steps, which produces an unfinished picture."""
for name in ("Tongyi-MAI/Z-Image", "stabilityai/stable-diffusion-xl-base-1.0",
"black-forest-labs/FLUX.2-klein-4B", "krea/Krea-2"):
assert distilled_defaults_for(name) is None, name
def test_a_word_inside_another_word_does_not_count():
assert distilled_defaults_for("John6666/turbovision-xl") is None
assert distilled_defaults_for("some/lcmx-model") is None
assert distilled_defaults_for("") is None
assert distilled_defaults_for(None) is None
def test_only_the_last_path_segment_is_read():
"""An org called "turbo-labs" does not make its models distilled."""
assert distilled_defaults_for("turbo-labs/plain-sdxl-finetune") is None
def test_the_card_and_the_request_both_outrank_the_name():
"""Checked on the source, because the precedence IS the behaviour: the
name is a guess, and a number an operator or a caller wrote is not."""
block = _SRC.split("_distilled = None", 1)[1][:2200]
assert "if not _d_steps and _accel_steps is None:" in block, (
"the name heuristic runs even when the card declared its own steps")
# In the expression itself, the distilled value must sit below the request,
# the fused LoRA and the card.
expr = block.split("num_steps = ", 1)[1].split("cfg_scale = ", 1)[0]
assert expr.index("request.steps") < expr.index("_accel_steps") < \
expr.index("_d_steps") < expr.index('_distilled["steps"]'), (
f"precedence is wrong in: {expr.strip()!r}")
assert expr.index('_distilled["steps"]') < expr.index("30 if quality"), (
"the generic 30-step fallback still wins over the distilled default")
def test_the_choice_is_logged_with_what_it_replaced():
"""An unexplained step count is indistinguishable from a bug when the
picture comes out wrong, so the line has to name both numbers."""
block = _SRC.split("_distilled = None", 1)[1][:2200]
assert "distilled" in block and "30 at CFG 7.5" in block
This diff is collapsed.
# CoderAI - OpenAI-compatible API server
# Copyright (C) 2026 Stefy Lanza <stefy@nexlab.net>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""The minimum-eviction block before an image load has to actually run.
Observed on zeiss, 2026-10-29, on every image load:
Proactive eviction skipped: name 'request' is not defined
``_load_diffusers_pipeline`` has no ``request`` in scope -- it takes
``(model_name, global_args, model_config)``. The sizing call read it anyway,
so the block raised NameError before it could evict anything, and the broad
``except`` printed the NameError as a skip. Every image load then fell through
to the blunt eviction further down, which frees by a different rule and
unloads models that would have fitted alongside.
A name error inside a try/except that reports "skipped" is invisible: the
feature is simply never there. These tests fail on the shape that allows it.
"""
import ast
import io
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_SRC = io.open(os.path.join(_ROOT, "codai/api/images.py"), encoding="utf-8").read()
def _fn(name):
for node in ast.walk(ast.parse(_SRC)):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name == name:
return node
raise AssertionError(f"{name} not found")
def test_the_loader_does_not_read_a_request_it_never_received():
"""Every name the function reads must be a parameter, a local, a global or
a builtin. `request` is none of those here."""
fn = _fn("_load_diffusers_pipeline")
params = {a.arg for a in fn.args.args} | {a.arg for a in fn.args.kwonlyargs}
assert "request" not in params
reads = [n for n in ast.walk(fn)
if isinstance(n, ast.Name) and n.id == "request" and isinstance(n.ctx, ast.Load)]
assert not reads, (
f"_load_diffusers_pipeline reads `request` at line(s) "
f"{[n.lineno for n in reads]}, which does not exist in its scope")
def test_the_requested_id_is_captured_before_the_path_rewrite():
"""Sizing needs the id the CALLER asked for: two cards can share one
checkpoint with different quantization and offload. `model_name` is
rewritten to the checkpoint path early on, so the id has to be kept
before that."""
fn = _fn("_load_diffusers_pipeline")
stores = [n.lineno for n in ast.walk(fn)
if isinstance(n, ast.Name) and n.id == "_requested_id"
and isinstance(n.ctx, ast.Store)]
loads = [n.lineno for n in ast.walk(fn)
if isinstance(n, ast.Name) and n.id == "_requested_id"
and isinstance(n.ctx, ast.Load)]
assert stores and loads, "the requested id is not kept at all"
rewrite = _SRC.index("model_name = _cfg_path")
rewrite_line = _SRC[:rewrite].count("\n") + 1
assert min(stores) < rewrite_line, (
f"_requested_id is assigned at line {min(stores)}, after model_name is "
f"rewritten to the checkpoint path at {rewrite_line} — it would hold "
f"the path, not the id that was asked for")
assert min(stores) < min(loads)
def test_the_sizing_call_is_still_made_with_it():
block = _SRC.split("Proactive VRAM eviction", 1)[1][:2500]
assert "requested_id=_requested_id" in block
assert "_evict_models_for_vram" in block
...@@ -39,12 +39,20 @@ def test_the_request_beats_the_card_and_the_card_beats_the_floor(): ...@@ -39,12 +39,20 @@ def test_the_request_beats_the_card_and_the_card_beats_the_floor():
from codai.api import images from codai.api import images
src = inspect.getsource(images._generate_with_diffusers) src = inspect.getsource(images._generate_with_diffusers)
# The window is the step/guidance decision itself -- from where the card is
# read to where the guidance ends -- not a fixed slice. A fixed slice fails
# the moment another layer is inserted between the two, which says nothing
# about the precedence this test is here to protect.
i = src.index("_dflt = model_defaults(model_config)") i = src.index("_dflt = model_defaults(model_config)")
window = src[i:i + 900] window = src[i:src.index("_progress_reset(", i)]
# request.steps first, then the fused-accel preset, then the card, then 30. # request.steps first, then the fused-accel preset, then the card, then 30.
assert "request.steps if request.steps" in window assert "request.steps if request.steps" in window
assert "_accel_steps if _accel_steps" in window assert "_accel_steps if _accel_steps" in window
assert "int(_d_steps) if _d_steps" in window assert "int(_d_steps) if _d_steps" in window
assert window.index("request.steps if request.steps") < \
window.index("_accel_steps if _accel_steps") < \
window.index("int(_d_steps) if _d_steps") < \
window.index("30 if quality")
def test_video_layers_the_card_under_the_accel_preset_and_over_the_floor(): def test_video_layers_the_card_under_the_accel_preset_and_over_the_floor():
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment