Add the diffusion auto-policy layer: per-family footprint estimates and the dense-quant re-plan
The loader used to plan memory from the GGUF file size and only offer the dense transformer-quant fast path when that plan was already resident, so on a card where the GGUF forced offload the int8/fp8 build (roughly half the bf16 bytes, or exactly the quantised size when a pre-quantized checkpoint exists) was never attempted. diffusion_auto_policy.py is a pure decision layer: a bf16-resident component table per family (transformer / text encoders / VAE, with base-repo overrides for the multi-size families), per-scheme size factors with separate steady and transient (build peak) numbers, and resolve_dense_quant_candidate which the loader now uses to re-plan memory against the candidate artifact before settling for offload. The engaged plan is adopted only when the dense build succeeds; the GGUF fallback keeps its own plan. Status now carries a resolved provenance record per Advanced control (value, source auto or explicit, reason) so the UI can label backend decisions.
This commit is contained in:
parent
e39ae8fb58
commit
a8e708f66e
3 changed files with 532 additions and 3 deletions
|
|
@ -59,6 +59,7 @@ from .diffusion_speed import (
|
|||
SPEED_OFF,
|
||||
apply_speed_optims,
|
||||
compile_eligible,
|
||||
normalize_speed_mode,
|
||||
resolve_speed_mode,
|
||||
restore_backend_flags,
|
||||
snapshot_backend_flags,
|
||||
|
|
@ -75,6 +76,7 @@ from .diffusion_prequant import (
|
|||
load_prequantized_transformer,
|
||||
resolve_prequant_source,
|
||||
)
|
||||
from .diffusion_auto_policy import build_resolved_record, resolve_dense_quant_candidate
|
||||
from .diffusion_transformer_quant import (
|
||||
DEFAULT_MIN_LINEAR_FEATURES,
|
||||
dense_transformer_supported,
|
||||
|
|
@ -283,6 +285,10 @@ class _LoadState:
|
|||
compile_cache_ctx: Any = None
|
||||
# Token kept so LoRA adapters selected at generate time can be fetched from the Hub.
|
||||
hf_token: Optional[str] = None
|
||||
# Per-control provenance from the auto-policy: {control: {value, source, reason}}.
|
||||
# source is "auto" when the backend decided (request left unset / "auto") and
|
||||
# "explicit" when the caller pinned the value. Surfaced via status for the UI badges.
|
||||
resolved: Optional[dict] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -971,11 +977,48 @@ class DiffusionBackend:
|
|||
# pipeline) do not have.
|
||||
pipe = None
|
||||
transformer_quant_engaged = None
|
||||
quant_plan = None
|
||||
if (
|
||||
kind == "gguf"
|
||||
and normalize_transformer_quant(transformer_quant) is not None
|
||||
and dense_transformer_supported(target)
|
||||
and plan.offload_policy == OFFLOAD_NONE
|
||||
and plan.offload_policy != OFFLOAD_NONE
|
||||
):
|
||||
# The GGUF-size plan picked offload, but the dense-quant artifact has
|
||||
# a DIFFERENT footprint: int8/fp8 weights are ~half the bf16 bytes, and
|
||||
# a pre-quantized checkpoint never materialises dense bf16 at all. Ask
|
||||
# the auto-policy for the candidate's estimate and re-plan against it:
|
||||
# a resident quantised build beats an offloaded GGUF on speed AND
|
||||
# quality, so it must be attempted before settling for offload.
|
||||
candidate = resolve_dense_quant_candidate(
|
||||
fam = fam,
|
||||
target = target,
|
||||
requested = transformer_quant,
|
||||
base_repo = base,
|
||||
prequant_path = transformer_prequant_path,
|
||||
logger = logger,
|
||||
)
|
||||
if candidate is not None:
|
||||
replanned = self._plan_memory(
|
||||
target,
|
||||
single_file_path,
|
||||
base,
|
||||
fam,
|
||||
memory_mode,
|
||||
cpu_offload,
|
||||
kind = kind,
|
||||
repo_id = repo_id,
|
||||
transformer_resident_override_mib = (
|
||||
candidate.transient_transformer_mib
|
||||
),
|
||||
)
|
||||
if replanned.offload_policy == OFFLOAD_NONE:
|
||||
quant_plan = replanned
|
||||
if (
|
||||
kind == "gguf"
|
||||
and normalize_transformer_quant(transformer_quant) is not None
|
||||
and dense_transformer_supported(target)
|
||||
and (plan.offload_policy == OFFLOAD_NONE or quant_plan is not None)
|
||||
):
|
||||
try:
|
||||
pipe, transformer_quant_engaged = self._load_dense_quant_pipeline(
|
||||
|
|
@ -1004,6 +1047,10 @@ class DiffusionBackend:
|
|||
# GGUF build (the OOM-fallback path this cleanup exists for).
|
||||
del exc
|
||||
clear_gpu_cache()
|
||||
if transformer_quant_engaged is not None and quant_plan is not None:
|
||||
# The re-planned resident placement is the one the engaged dense build
|
||||
# actually uses; the GGUF-size plan stays in force for the fallback.
|
||||
plan = quant_plan
|
||||
|
||||
if pipe is None:
|
||||
if kind == "pipeline":
|
||||
|
|
@ -1228,6 +1275,57 @@ class DiffusionBackend:
|
|||
pipe, plan, device = device, logger = logger
|
||||
)
|
||||
|
||||
# Per-control provenance for status: what engaged and who decided it
|
||||
# (the caller, or this backend's auto resolution). cpu_offload=False is
|
||||
# the unset default, so only True counts as an explicit request.
|
||||
resolved = build_resolved_record(
|
||||
{
|
||||
"speed_mode": (
|
||||
speed_mode,
|
||||
effective_speed,
|
||||
"quantized transformer requires compile"
|
||||
if transformer_quant_engaged is not None
|
||||
and normalize_speed_mode(speed_mode) in (None, SPEED_OFF)
|
||||
else "per-kind default"
|
||||
if speed_mode is None
|
||||
else "requested",
|
||||
),
|
||||
"transformer_quant": (
|
||||
transformer_quant,
|
||||
transformer_quant_engaged or "off",
|
||||
"not engaged (GGUF transformer loaded)"
|
||||
if transformer_quant_engaged is None
|
||||
else "re-planned resident for the quantised artifact"
|
||||
if quant_plan is not None
|
||||
else "engaged on the dense fast path",
|
||||
),
|
||||
"attention_backend": (
|
||||
attention_backend,
|
||||
attention_engaged or "native",
|
||||
"cuDNN fused attention upgrade"
|
||||
if attention_engaged and attention_backend is None
|
||||
else "diffusers default"
|
||||
if attention_engaged is None
|
||||
else "requested",
|
||||
),
|
||||
"memory_mode": (
|
||||
memory_mode,
|
||||
effective_policy,
|
||||
"planned from measured free VRAM vs estimated footprint",
|
||||
),
|
||||
"transformer_cache": (
|
||||
transformer_cache,
|
||||
cache_engaged or "off",
|
||||
"off by default" if transformer_cache is None else "requested",
|
||||
),
|
||||
"cpu_offload": (
|
||||
True if cpu_offload else None,
|
||||
effective_policy != OFFLOAD_NONE,
|
||||
"legacy flag" if cpu_offload else "from the memory plan",
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
self._state = _LoadState(
|
||||
pipe = pipe,
|
||||
family = fam,
|
||||
|
|
@ -1250,6 +1348,7 @@ class DiffusionBackend:
|
|||
eager_patched = eager_patched,
|
||||
compile_cache_ctx = compile_ctx,
|
||||
hf_token = hf_token,
|
||||
resolved = resolved,
|
||||
)
|
||||
state_committed = True
|
||||
finally:
|
||||
|
|
@ -1383,6 +1482,7 @@ class DiffusionBackend:
|
|||
*,
|
||||
kind: str = "gguf",
|
||||
repo_id: Optional[str] = None,
|
||||
transformer_resident_override_mib: Optional[int] = None,
|
||||
):
|
||||
"""Build the memory plan for this load: snapshot free device memory and
|
||||
estimate the model's resident footprint, then let the planner pick an
|
||||
|
|
@ -1392,7 +1492,10 @@ class DiffusionBackend:
|
|||
The size estimate is per-kind: diffusers keeps GGUF weights packed (per-matmul
|
||||
transient dequant), so a GGUF loads near its on-disk size; a safetensors
|
||||
single-file loads near its on-disk size (it carries its dtype); and a full
|
||||
pipeline is one cached download (transformer + companions), already compressed."""
|
||||
pipeline is one cached download (transformer + companions), already compressed.
|
||||
``transformer_resident_override_mib`` replaces the file-size transformer estimate
|
||||
when the loader is planning for a DIFFERENT artifact than the file on disk (the
|
||||
dense transformer-quant candidate, whose footprint the auto-policy estimates)."""
|
||||
device_memory = snapshot_device_memory(target)
|
||||
if kind == "pipeline":
|
||||
# The whole repo (transformer + companions) is one cached download; the
|
||||
|
|
@ -1408,7 +1511,12 @@ class DiffusionBackend:
|
|||
model_dense_mib = estimate_safetensors_dense_mib(cached_mib)
|
||||
companion_mib = None
|
||||
else:
|
||||
if kind == "single_file":
|
||||
if transformer_resident_override_mib is not None:
|
||||
# Planning for a different artifact than the file on disk (the dense
|
||||
# transformer-quant candidate): the auto-policy's estimate replaces the
|
||||
# file-size derivation; companions below stay measured from the cache.
|
||||
transformer_resident = transformer_resident_override_mib
|
||||
elif kind == "single_file":
|
||||
# Safetensors single-file: no dequant expansion (it carries its dtype).
|
||||
transformer_resident = estimate_safetensors_dense_mib(
|
||||
file_size_mib(single_file_path)
|
||||
|
|
@ -2133,6 +2241,7 @@ class DiffusionBackend:
|
|||
"workflows": [],
|
||||
"supports_lora": False,
|
||||
"supports_controlnet": False,
|
||||
"resolved": None,
|
||||
}
|
||||
from core.inference import diffusion_controlnet, diffusion_lora
|
||||
|
||||
|
|
@ -2154,6 +2263,7 @@ class DiffusionBackend:
|
|||
"transformer_quant": state.transformer_quant,
|
||||
"attention_backend": state.attention_backend,
|
||||
"transformer_cache": state.transformer_cache,
|
||||
"resolved": state.resolved,
|
||||
# Image-conditioned workflows the loaded family supports, so the UI can gate
|
||||
# its tabs. txt2img is always available on the diffusers engine.
|
||||
"workflows": _family_workflows(state.family),
|
||||
|
|
|
|||
206
studio/backend/core/inference/diffusion_auto_policy.py
Normal file
206
studio/backend/core/inference/diffusion_auto_policy.py
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""Hardware-aware auto-policy for the diffusion loader: a pure decision layer.
|
||||
|
||||
The loader historically resolved each Advanced control on its own, and -- critically --
|
||||
planned memory from the GGUF file size BEFORE the dense transformer-quant fast path was
|
||||
considered. That ordering hid the fast path exactly where it matters: on a card where the
|
||||
GGUF-size plan picks offload, a dense int8/fp8 transformer (roughly half the bf16 bytes)
|
||||
would often still fit fully resident and beat the offloaded GGUF on every axis.
|
||||
|
||||
This module supplies the two pieces the loader needs to fix that, without moving any of
|
||||
the existing per-control executors:
|
||||
|
||||
* a per-family bf16 component-size table (transformer / text encoders / VAE) with
|
||||
per-scheme scaling, so the candidate artifact's footprint can be estimated BEFORE
|
||||
anything is downloaded or materialised; and
|
||||
* ``resolve_dense_quant_candidate``, which turns a request + device into a concrete
|
||||
(scheme, steady, transient) estimate the loader re-plans memory against.
|
||||
|
||||
It also builds the ``resolved`` record surfaced through status: for every Advanced
|
||||
control, the engaged value plus whether it came from the user (explicit) or the policy
|
||||
(auto), with a short reason. Pure by design: no torch import at module import time, so
|
||||
the decision logic unit-tests on CPU-only hosts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional
|
||||
|
||||
_MIB_PER_GB = 1000.0 ** 3 / (1024.0 * 1024.0) # component sizes below are decimal GB
|
||||
|
||||
# Steady-state size of a torchao-quantised transformer relative to its bf16 weights:
|
||||
# int8 / fp8 store one byte per param plus per-row scales (~0.52x) with a little slack
|
||||
# for non-quantised modules (norms, embeddings, proj_out stay bf16); nvfp4 packs two
|
||||
# params per byte plus block scales. Measured on the live int8/fp8 loads this session.
|
||||
_QUANT_STEADY_FACTOR: dict[str, float] = {
|
||||
"int8": 0.55,
|
||||
"fp8": 0.55,
|
||||
"mxfp8": 0.58,
|
||||
"nvfp4": 0.33,
|
||||
}
|
||||
|
||||
# bf16-RESIDENT component sizes in decimal GB: (transformer, text encoders, VAE).
|
||||
# These are what the components occupy on device after the loader's dtype cast, NOT the
|
||||
# repo download size (Z-Image ships its transformer in fp32: 24.6 GB of shards that load
|
||||
# as 12.3 GB bf16). Sourced from the HF sibling metadata of each family's base repo with
|
||||
# precision duplicates removed, cross-checked against the training-side dense_bf16_gb
|
||||
# table and the measured loads in this repo's GPU verification runs.
|
||||
_FAMILY_BF16_GB: dict[str, tuple[float, float, float]] = {
|
||||
"flux.1": (23.8, 9.8, 0.2),
|
||||
"flux.1-kontext": (23.8, 9.8, 0.2),
|
||||
"flux.2-klein": (7.8, 8.0, 0.2),
|
||||
"flux.2-dev": (64.5, 48.0, 0.4),
|
||||
"qwen-image": (40.9, 16.6, 0.3),
|
||||
"qwen-image-edit": (40.9, 16.6, 0.3),
|
||||
"z-image": (12.3, 8.0, 0.2),
|
||||
"krea-2": (26.3, 8.9, 0.5),
|
||||
}
|
||||
|
||||
# Base-repo overrides for families whose picker offers multiple sizes under one family
|
||||
# entry (the table above carries the family default base).
|
||||
_BASE_REPO_BF16_GB: dict[str, tuple[float, float, float]] = {
|
||||
"black-forest-labs/FLUX.2-klein-9B": (18.2, 16.4, 0.2),
|
||||
}
|
||||
|
||||
|
||||
def family_bf16_components_gb(
|
||||
fam: Any, base_repo: Optional[str] = None
|
||||
) -> Optional[tuple[float, float, float]]:
|
||||
"""(transformer, text encoders, VAE) bf16-resident sizes in GB, or None when the
|
||||
family is not in the table (callers must then fall back to file-size estimates)."""
|
||||
if base_repo:
|
||||
override = _BASE_REPO_BF16_GB.get(base_repo)
|
||||
if override is not None:
|
||||
return override
|
||||
name = getattr(fam, "name", None)
|
||||
return _FAMILY_BF16_GB.get(name) if name else None
|
||||
|
||||
|
||||
@dataclass(frozen = True)
|
||||
class DenseQuantEstimate:
|
||||
"""Footprint estimate for one dense transformer-quant candidate.
|
||||
|
||||
``transient_transformer_mib`` is the build peak: the dense bf16 transformer when
|
||||
quantising on the fly, or the quantised size itself when a pre-quantized checkpoint
|
||||
is available (it loads via the meta device, so dense bf16 never lands on the GPU).
|
||||
``steady_transformer_mib`` is what stays resident for generation."""
|
||||
|
||||
scheme: str
|
||||
steady_transformer_mib: int
|
||||
transient_transformer_mib: int
|
||||
companions_mib: int
|
||||
prequant: bool
|
||||
|
||||
@property
|
||||
def transient_total_mib(self) -> int:
|
||||
return self.transient_transformer_mib + self.companions_mib
|
||||
|
||||
@property
|
||||
def steady_total_mib(self) -> int:
|
||||
return self.steady_transformer_mib + self.companions_mib
|
||||
|
||||
|
||||
def estimate_dense_quant(
|
||||
fam: Any,
|
||||
scheme: str,
|
||||
*,
|
||||
base_repo: Optional[str] = None,
|
||||
prequant_available: bool = False,
|
||||
) -> Optional[DenseQuantEstimate]:
|
||||
"""Estimate the candidate's footprint from the family table, or None when the
|
||||
family (or scheme factor) is unknown."""
|
||||
components = family_bf16_components_gb(fam, base_repo)
|
||||
factor = _QUANT_STEADY_FACTOR.get(scheme)
|
||||
if components is None or factor is None:
|
||||
return None
|
||||
transformer_gb, text_encoders_gb, vae_gb = components
|
||||
steady = int(transformer_gb * factor * _MIB_PER_GB)
|
||||
transient = steady if prequant_available else int(transformer_gb * _MIB_PER_GB)
|
||||
companions = int((text_encoders_gb + vae_gb) * _MIB_PER_GB)
|
||||
return DenseQuantEstimate(
|
||||
scheme = scheme,
|
||||
steady_transformer_mib = steady,
|
||||
transient_transformer_mib = transient,
|
||||
companions_mib = companions,
|
||||
prequant = prequant_available,
|
||||
)
|
||||
|
||||
|
||||
def resolve_dense_quant_candidate(
|
||||
*,
|
||||
fam: Any,
|
||||
target: Any,
|
||||
requested: Optional[str],
|
||||
base_repo: Optional[str] = None,
|
||||
prequant_path: Optional[str] = None,
|
||||
logger: Optional[logging.Logger] = None,
|
||||
) -> Optional[DenseQuantEstimate]:
|
||||
"""The dense-quant candidate the loader should re-plan memory against, or None.
|
||||
|
||||
None means "no basis to re-plan": the request is off, the device cannot run the
|
||||
dense path, no scheme resolves, or the family has no size entry. The loader then
|
||||
keeps today's behaviour (fast path only when the GGUF-size plan is already
|
||||
resident), so unlisted families see no change."""
|
||||
from .diffusion_transformer_quant import (
|
||||
dense_transformer_supported,
|
||||
normalize_transformer_quant,
|
||||
select_transformer_quant_scheme,
|
||||
)
|
||||
|
||||
if normalize_transformer_quant(requested) is None:
|
||||
return None
|
||||
if not dense_transformer_supported(target):
|
||||
return None
|
||||
scheme = select_transformer_quant_scheme(target, requested)
|
||||
if scheme is None:
|
||||
return None
|
||||
prequant_available = False
|
||||
try:
|
||||
from .diffusion_prequant import resolve_prequant_source
|
||||
|
||||
prequant_available = (
|
||||
resolve_prequant_source(fam, scheme, path_override = prequant_path) is not None
|
||||
)
|
||||
except Exception: # noqa: BLE001 -- prequant probing must never sink the candidate
|
||||
prequant_available = False
|
||||
estimate = estimate_dense_quant(
|
||||
fam, scheme, base_repo = base_repo, prequant_available = prequant_available
|
||||
)
|
||||
if estimate is not None and logger is not None:
|
||||
logger.info(
|
||||
"diffusion.auto_policy: dense %s candidate steady=%d MiB transient=%d MiB "
|
||||
"companions=%d MiB prequant=%s",
|
||||
scheme,
|
||||
estimate.steady_transformer_mib,
|
||||
estimate.transient_transformer_mib,
|
||||
estimate.companions_mib,
|
||||
prequant_available,
|
||||
)
|
||||
return estimate
|
||||
|
||||
|
||||
# ── resolved-record (status surface) ─────────────────────────────────────────
|
||||
def build_resolved_record(
|
||||
controls: dict[str, tuple[Optional[Any], Any, str]],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""The per-control ``resolved`` record for status: engaged value + provenance.
|
||||
|
||||
``controls`` maps a control name to ``(explicit, engaged, reason)`` where
|
||||
``explicit`` is the raw request value (None / "" / "auto" meaning the caller left
|
||||
the decision to the backend) and ``engaged`` is what actually applied. The record
|
||||
is what the frontend renders as an "Auto: X" badge next to each Advanced row."""
|
||||
record: dict[str, dict[str, Any]] = {}
|
||||
for name, (explicit, engaged, reason) in controls.items():
|
||||
left_to_backend = explicit is None or (
|
||||
isinstance(explicit, str) and explicit.strip().lower() in ("", "auto")
|
||||
)
|
||||
record[name] = {
|
||||
"value": engaged,
|
||||
"source": "auto" if left_to_backend else "explicit",
|
||||
"reason": reason,
|
||||
}
|
||||
return record
|
||||
213
studio/backend/tests/test_diffusion_auto_policy.py
Normal file
213
studio/backend/tests/test_diffusion_auto_policy.py
Normal file
|
|
@ -0,0 +1,213 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""CPU-only unit tests for the diffusion auto-policy decision layer.
|
||||
|
||||
Covers the per-family footprint estimator (bf16-resident component sizes x per-scheme
|
||||
factors, transient vs steady, base-repo overrides), the dense-quant candidate resolution
|
||||
(with the quant selector / prequant probe monkeypatched, no torch), and the resolved
|
||||
provenance record. The loader-side ordering fix is exercised through the planner: the
|
||||
regression case is a GGUF whose file-size plan forces offload while the candidate's
|
||||
estimate fits resident."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import core.inference.diffusion_auto_policy as ap
|
||||
from core.inference.diffusion_auto_policy import (
|
||||
DenseQuantEstimate,
|
||||
build_resolved_record,
|
||||
estimate_dense_quant,
|
||||
family_bf16_components_gb,
|
||||
resolve_dense_quant_candidate,
|
||||
)
|
||||
from core.inference.diffusion_memory import (
|
||||
OFFLOAD_NONE,
|
||||
DeviceMemory,
|
||||
plan_diffusion_memory,
|
||||
)
|
||||
|
||||
|
||||
def _fam(name = "z-image"):
|
||||
return SimpleNamespace(name = name)
|
||||
|
||||
|
||||
# ── the per-family table ──────────────────────────────────────────────────────
|
||||
def test_family_table_covers_the_dit_families():
|
||||
for name in ("flux.1", "flux.2-klein", "flux.2-dev", "qwen-image", "z-image", "krea-2"):
|
||||
comps = family_bf16_components_gb(_fam(name))
|
||||
assert comps is not None, f"{name} missing from the bf16 component table"
|
||||
transformer, text_encoders, vae = comps
|
||||
assert transformer > 1.0 and text_encoders > 0.0 and vae > 0.0
|
||||
|
||||
|
||||
def test_family_table_unknown_family_returns_none():
|
||||
assert family_bf16_components_gb(_fam("not-a-family")) is None
|
||||
|
||||
|
||||
def test_base_repo_override_wins_over_the_family_default():
|
||||
# flux.2-klein's family default is the 4B base; loading the 9B GGUF passes the 9B
|
||||
# base repo, whose transformer is more than twice the size.
|
||||
default = family_bf16_components_gb(_fam("flux.2-klein"))
|
||||
nine_b = family_bf16_components_gb(
|
||||
_fam("flux.2-klein"), base_repo = "black-forest-labs/FLUX.2-klein-9B"
|
||||
)
|
||||
assert nine_b is not None and default is not None
|
||||
assert nine_b[0] > 2 * default[0]
|
||||
|
||||
|
||||
# ── the estimator ─────────────────────────────────────────────────────────────
|
||||
def test_estimate_int8_steady_is_roughly_half_bf16():
|
||||
est = estimate_dense_quant(_fam("z-image"), "int8")
|
||||
assert est is not None
|
||||
bf16_mib = 12.3 * ap._MIB_PER_GB
|
||||
assert 0.5 * bf16_mib < est.steady_transformer_mib < 0.6 * bf16_mib
|
||||
# On-the-fly quantisation transiently materialises the dense bf16 transformer.
|
||||
assert est.transient_transformer_mib == int(bf16_mib)
|
||||
assert est.prequant is False
|
||||
|
||||
|
||||
def test_estimate_prequant_transient_equals_steady():
|
||||
# A pre-quantized checkpoint loads via the meta device: dense bf16 never lands on
|
||||
# the GPU, so the build peak IS the quantised size.
|
||||
est = estimate_dense_quant(_fam("z-image"), "int8", prequant_available = True)
|
||||
assert est is not None
|
||||
assert est.transient_transformer_mib == est.steady_transformer_mib
|
||||
assert est.prequant is True
|
||||
|
||||
|
||||
def test_estimate_nvfp4_is_smaller_than_int8():
|
||||
int8 = estimate_dense_quant(_fam("flux.1"), "int8")
|
||||
nvfp4 = estimate_dense_quant(_fam("flux.1"), "nvfp4")
|
||||
assert int8 is not None and nvfp4 is not None
|
||||
assert nvfp4.steady_transformer_mib < int8.steady_transformer_mib
|
||||
|
||||
|
||||
def test_estimate_unknown_family_or_scheme_returns_none():
|
||||
assert estimate_dense_quant(_fam("not-a-family"), "int8") is None
|
||||
assert estimate_dense_quant(_fam("z-image"), "q4_k") is None
|
||||
|
||||
|
||||
# ── candidate resolution (selector + prequant probe stubbed) ─────────────────
|
||||
def _patch_selector(monkeypatch, *, supported = True, scheme = "int8", prequant = None):
|
||||
import core.inference.diffusion_transformer_quant as tq
|
||||
|
||||
monkeypatch.setattr(tq, "dense_transformer_supported", lambda target: supported)
|
||||
monkeypatch.setattr(tq, "select_transformer_quant_scheme", lambda target, req: scheme)
|
||||
import core.inference.diffusion_prequant as pq
|
||||
|
||||
monkeypatch.setattr(
|
||||
pq, "resolve_prequant_source", lambda fam, s, path_override = None: prequant
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_resolves_for_a_supported_request(monkeypatch):
|
||||
_patch_selector(monkeypatch, scheme = "int8")
|
||||
est = resolve_dense_quant_candidate(
|
||||
fam = _fam("z-image"), target = object(), requested = "auto"
|
||||
)
|
||||
assert isinstance(est, DenseQuantEstimate)
|
||||
assert est.scheme == "int8"
|
||||
assert est.transient_transformer_mib > est.steady_transformer_mib
|
||||
|
||||
|
||||
def test_candidate_none_when_request_is_off(monkeypatch):
|
||||
_patch_selector(monkeypatch)
|
||||
for off in (None, "", "none", "off"):
|
||||
assert (
|
||||
resolve_dense_quant_candidate(fam = _fam(), target = object(), requested = off)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_none_when_device_unsupported(monkeypatch):
|
||||
_patch_selector(monkeypatch, supported = False)
|
||||
assert (
|
||||
resolve_dense_quant_candidate(fam = _fam(), target = object(), requested = "auto")
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_none_when_no_scheme_resolves(monkeypatch):
|
||||
_patch_selector(monkeypatch, scheme = None)
|
||||
assert (
|
||||
resolve_dense_quant_candidate(fam = _fam(), target = object(), requested = "auto")
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_none_for_an_unlisted_family(monkeypatch):
|
||||
# No size entry -> no basis to re-plan; the loader keeps today's resident-only gate.
|
||||
_patch_selector(monkeypatch)
|
||||
assert (
|
||||
resolve_dense_quant_candidate(
|
||||
fam = _fam("not-a-family"), target = object(), requested = "auto"
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candidate_uses_prequant_transient_when_available(monkeypatch):
|
||||
_patch_selector(monkeypatch, prequant = object())
|
||||
est = resolve_dense_quant_candidate(
|
||||
fam = _fam("z-image"), target = object(), requested = "int8"
|
||||
)
|
||||
assert est is not None and est.prequant is True
|
||||
assert est.transient_transformer_mib == est.steady_transformer_mib
|
||||
|
||||
|
||||
# ── the ordering-fix regression, at the planner level ─────────────────────────
|
||||
def _cuda_target():
|
||||
return SimpleNamespace(device = "cuda", supports_model_cpu_offload = True)
|
||||
|
||||
|
||||
def test_quant_candidate_fits_resident_where_gguf_plan_offloads():
|
||||
# The ordering-fix mechanism, on a 32 GiB consumer card (RTX 5090 class): the user
|
||||
# picked a LARGE GGUF (the BF16 file), so the file-size plan forces offload -- but
|
||||
# the dense-quant candidate is far smaller (int8 prequant of z-image: the transient
|
||||
# IS the quantised size), and re-planning against the candidate keeps everything
|
||||
# resident. Before the fix the loader never attempted the fast path here.
|
||||
memory = DeviceMemory("cuda", "cuda", "discrete_vram", 30000, 32768)
|
||||
z_bf16_gguf_mib = int(12.3 * ap._MIB_PER_GB * 1.05) # BF16 GGUF resident estimate
|
||||
companions_mib = 2600 # fp8-quantised text encoders + VAE
|
||||
gguf_plan = plan_diffusion_memory(
|
||||
target = _cuda_target(),
|
||||
device_memory = memory,
|
||||
model_dense_mib = z_bf16_gguf_mib + companions_mib,
|
||||
companion_dense_mib = companions_mib,
|
||||
runtime_headroom_mib = 6963,
|
||||
)
|
||||
assert gguf_plan.offload_policy != OFFLOAD_NONE
|
||||
|
||||
est = estimate_dense_quant(_fam("z-image"), "int8", prequant_available = True)
|
||||
assert est is not None
|
||||
assert est.transient_transformer_mib < z_bf16_gguf_mib / 1.8
|
||||
quant_plan = plan_diffusion_memory(
|
||||
target = _cuda_target(),
|
||||
device_memory = memory,
|
||||
model_dense_mib = est.transient_transformer_mib + companions_mib,
|
||||
companion_dense_mib = companions_mib,
|
||||
runtime_headroom_mib = 6963,
|
||||
)
|
||||
assert quant_plan.offload_policy == OFFLOAD_NONE
|
||||
|
||||
|
||||
# ── the resolved provenance record ────────────────────────────────────────────
|
||||
def test_resolved_record_marks_auto_and_explicit():
|
||||
record = build_resolved_record(
|
||||
{
|
||||
"speed_mode": (None, "default", "per-kind default"),
|
||||
"transformer_quant": ("auto", "fp8", "auto ladder"),
|
||||
"attention_backend": ("cudnn", "_native_cudnn", "requested"),
|
||||
"memory_mode": ("", "none", "planned"),
|
||||
"cpu_offload": (True, True, "legacy flag"),
|
||||
}
|
||||
)
|
||||
assert record["speed_mode"]["source"] == "auto"
|
||||
assert record["transformer_quant"]["source"] == "auto" # "auto" delegates to backend
|
||||
assert record["attention_backend"]["source"] == "explicit"
|
||||
assert record["memory_mode"]["source"] == "auto" # blank string delegates
|
||||
assert record["cpu_offload"]["source"] == "explicit"
|
||||
assert record["transformer_quant"]["value"] == "fp8"
|
||||
assert all("reason" in v for v in record.values())
|
||||
Loading…
Add table
Add a link
Reference in a new issue