motif64

m64_2804b385e240 / moss_image_server.py

344 lines, 18.1 KBraw
1"""OpenAI-compatible /v1/images/generations front end for the MOSS-Image family.23ONE SERVER, SEVERAL CHECKPOINTS. MOSS-Image-Base, MOSS-Image-SFT (OpenMOSS-Team) and4Kromoss (lodestones) share an architecture, an inference pipeline, a text encoder, a5VAE and a repo layout, so which one is served is purely MODEL_DIR. Kromoss is a full6fine-tune of MOSS-Image-Base at 1024 px — same architecture, pipeline, text7encoder (Qwen3.5-9B) and VAE (the FLUX.2 SMALL decoder) as MOSS-Image-SFT, published as8the EMA weights at update 11000 over 704k training images. Author-measured on9PartiPrompts (100 prompts, CLIP ViT-L/14): 27.90 against MOSS-Image-Base's 27.19 at 5010steps, and 28.13 with the DMD2 LoRA.1112THREE SAMPLER CONTRACTS OVER ONE SET OF WEIGHTS, selected by MOSS_LORA, straight from13the README's settings table:1415    ""      full model   50 steps, CFG 3, negative branch skipping layer 2216    dmd2     + LoRA        8 steps, CFG 1, NO negative branch      <- the author's pick17    dmd      + LoRA        8 steps, CFG 1, NO negative branch18    base-dmd2  + LoRA      8 steps, CFG 1, NO negative branch, OWN SAMPLER (below)1920Flow-matching Euler with shift 5 for the first three; shift is not a request field because21it already lives in the scheduler config, and the pipeline reads it from there.2223base-dmd2 is data-archetype/moss_image_base_dmd2: a separate 8-step DMD2 LoRA (rank 128,24256 on ffn_up_gate) distilled on MOSS-Image-Base, shipped as a PEFT adapter. Its card is25explicit that it was trained for ONE sampler and others give worse results: at each step26jump to the clean estimate and re-noise it with FRESH noise to the next level (ComfyUI's27lcm), over 8 evenly spaced times under flow shift e^1.5 = 4.4817 (not 5). FreshNoiseScheduler28below is the card's class; the re-noise draws from a per-request CUDA generator seeded29from the request seed, so a seed still reproduces an image.3031uncond_skip_layers IS NOT A NEGATIVE PROMPT KNOB. It defaults to (22,) and REPLACES CFG's32negative-prompt prediction with a skipped-block prediction of the positive prompt, which33is why the full model still costs two forwards per step while ignoring what you put in34negative_prompt. Pass null or [] to get an ordinary negative branch back. Under the35LoRAs, guidance_scale is 1.0 so there is no second branch at all and the field is moot —36the server sets it to null for them, matching the README.3738THE LoRAs ARE APPLIED UNMERGED, VIA FORWARD HOOKS, using the author's own apply_lora from39the model card. That is not a style choice: the card is explicit that the delta is close40to bf16's rounding step, so merging it into the bf16 weights would quietly lose most of41it. Both are rank 128 / alpha 128 fp32 with kohya keys, and only one is used at a time.4243The text encoder is Qwen3.5-9B loaded as a transformers Qwen3_5Model — a PIPELINE44COMPONENT, not a served LLM, so this rig's vLLM gap on the qwen3_5 architecture (the wall45q-judger and the FLUX 3 prompt upsampler both hit) never applies here.4647Dtypes follow the card exactly: transformer and text encoder in bfloat16, VAE in float32,48and the initial noise generated on CPU in float32.4950LICENSE: Apache-2.0 for both the weights and the bundled inference code51(pipeline_moss_image.py, from MOSS-Image-SFT), and not gated — one of the genuinely52unencumbered image models on this rig, unlike flux3-klein (non-commercial) or lens (MIT53but research-only).5455Defaults are hardcoded below and echoed per call under "params_used"; query them via56GET /v1/config.57"""58import base64, io, os, threading, time59from typing import List, Optional6061import torch62from fastapi import FastAPI, HTTPException63from fastapi.responses import JSONResponse64from pydantic import BaseModel, Field65from safetensors.torch import load_file66import uvicorn67from diffusers import DiffusionPipeline6869# lora file, default steps, default CFG, default uncond_skip_layers70VARIANTS = {71    "":     {"lora": None, "steps": 50, "cfg": 3.0, "skip": [22],72             "about": "full model, negative branch skipping layer 22"},73    "dmd2": {"lora": "8-steps-dmd2-lora.safetensors", "steps": 8, "cfg": 1.0, "skip": None,74             "about": "8-step DMD2 turbo LoRA (DMD plus a GAN stage against real images)"},75    "dmd":  {"lora": "8-steps-dmd-lora.safetensors", "steps": 8, "cfg": 1.0, "skip": None,76             "about": "8-step data-space DMD turbo LoRA"},77    "base-dmd2": {"lora": "transformer/adapter_model.safetensors", "format": "peft",78                  "sampler": "fresh-noise", "steps": 8, "cfg": 1.0, "skip": None,79                  "about": "data-archetype's 8-step DMD2 LoRA for MOSS-Image-Base, fresh-noise sampler"},80}8182MOSS_LORA = os.environ.get("MOSS_LORA", "")83# The turbo LoRAs ship in the Kromoss repo but were distilled from84# MOSS-Image-Base, so they transfer across the family; point this at whichever85# directory actually holds them.86MOSS_LORA_DIR = os.environ.get("MOSS_LORA_DIR", os.environ.get("MODEL_DIR", "/model"))87if MOSS_LORA not in VARIANTS:88    raise SystemExit(f"MOSS_LORA must be one of {list(VARIANTS)}; got {MOSS_LORA!r}")89SPEC = VARIANTS[MOSS_LORA]90MODEL_DIR = os.environ.get("MODEL_DIR", "/model")91PORT = int(os.environ.get("PORT", "9218"))92SERVED_MODEL_ID = os.environ.get("SERVED_MODEL_ID",93                                 "moss-image" + (f"-{MOSS_LORA}" if MOSS_LORA else ""))94VAE_TILE = 16   # the pipeline rejects height/width that are not multiples of 169596app = FastAPI()97_lock = threading.Lock()98_state = {}99100101def apply_lora(transformer, path, strength=1.0):102    """Kromoss's own apply_lora, verbatim in behaviour.103104    Registers a forward hook per adapted module instead of merging, because the card105    states the delta is close to bf16's rounding step — merging would lose it.106    """107    state = load_file(path, device="cuda")108    modules = dict(transformer.named_modules())109    n = 0110    for key in [k for k in state if k.endswith(".lora_down.weight")]:111        name = key[len("diffusion_model."):-len(".lora_down.weight")]112        down, up = state[key].float(), state[key.replace("lora_down", "lora_up")].float()113        scale = strength * state[f"diffusion_model.{name}.alpha"].item() / down.shape[0]114115        def hook(module, args, out, down=down, up=up, scale=scale):116            return out + (scale * (args[0].float() @ down.T) @ up.T).to(out.dtype)117118        modules[name].register_forward_hook(hook)119        n += 1120    return n121122123def apply_peft_lora(transformer, adapter_dir, strength=1.0):124    """A PEFT LoRA adapter, applied unmerged through forward hooks like apply_lora.125126    Same reason as above: the delta is below bf16's rounding step, so it is never merged.127    Scale per module is lora_alpha / r with the adapter's rank/alpha patterns (rsLoRA and128    DoRA are refused rather than silently mis-applied).129    """130    import json, re131    cfg = json.load(open(os.path.join(adapter_dir, "adapter_config.json")))132    if cfg.get("use_dora") or cfg.get("use_rslora"):133        raise SystemExit("adapter uses DoRA/rsLoRA, which apply_peft_lora does not implement")134    state = load_file(os.path.join(adapter_dir, "adapter_model.safetensors"), device="cuda")135    modules = dict(transformer.named_modules())136137    def pattern(table, name, default):138        return next((v for k, v in table.items() if re.search(rf"(^|\.){re.escape(k)}$", name)), default)139140    n = 0141    for key in [k for k in state if k.endswith(".lora_A.weight")]:142        name = key[len("base_model.model."):-len(".lora_A.weight")]143        if name not in modules:144            raise SystemExit(f"adapter targets {name!r}, which the transformer does not have")145        down, up = state[key].float(), state[key.replace("lora_A", "lora_B")].float()146        r = pattern(cfg.get("rank_pattern") or {}, name, cfg["r"])147        alpha = pattern(cfg.get("alpha_pattern") or {}, name, cfg["lora_alpha"])148        if down.shape[0] != r:149            raise SystemExit(f"{name}: rank {down.shape[0]} disagrees with adapter_config ({r})")150        scale = strength * alpha / r151152        def hook(module, args, out, down=down, up=up, scale=scale):153            return out + (scale * (args[0].float() @ down.T) @ up.T).to(out.dtype)154155        modules[name].register_forward_hook(hook)156        n += 1157    return n158159160def _fresh_noise_scheduler():161    """The base-dmd2 card's FreshNoiseScheduler, with a seedable re-noise generator."""162    import math163    from diffusers import FlowMatchEulerDiscreteScheduler164    shift = math.exp(1.5)165166    class FreshNoiseScheduler(FlowMatchEulerDiscreteScheduler):167        generator = None  # set per request168169        def set_timesteps(self, num_inference_steps=8, device=None):170            t = torch.linspace(1.0, 1.0 / num_inference_steps, num_inference_steps)171            sigmas = shift * t / (1 + (shift - 1) * t)172            super().set_timesteps(device=device, sigmas=sigmas.tolist())173174        def step(self, model_output, timestep, sample, return_dict=True, **kwargs):175            if self.step_index is None:176                self._init_step_index(timestep)177            sigma, sigma_next = self.sigmas[self.step_index], self.sigmas[self.step_index + 1]178            clean = sample.float() - sigma * model_output.float()179            if sigma_next > 0:180                noise = torch.randn(clean.shape, generator=self.generator,181                                    device=clean.device, dtype=clean.dtype)182                clean = (1 - sigma_next) * clean + sigma_next * noise183            self._step_index += 1184            return (clean.to(sample.dtype),)185186    return FreshNoiseScheduler(shift=1.0), shift187188189def _load():190    t0 = time.time()191    pipe = DiffusionPipeline.from_pretrained(192        MODEL_DIR, custom_pipeline="pipeline_moss_image", trust_remote_code=True,193        torch_dtype={"default": torch.bfloat16, "vae": torch.float32},194    )195    # The card rebuilds the scheduler from its own config before use.196    pipe.scheduler = type(pipe.scheduler).from_config(pipe.scheduler.config)197    shift = (getattr(pipe.scheduler.config, "shift", None)198             if not isinstance(pipe.scheduler.config, dict) else pipe.scheduler.config.get("shift"))199    if SPEC.get("sampler") == "fresh-noise":200        pipe.scheduler, shift = _fresh_noise_scheduler()201    pipe.to("cuda")202    adapted = 0203    if SPEC.get("format") == "peft":204        adapted = apply_peft_lora(pipe.transformer,205                                  os.path.dirname(os.path.join(MOSS_LORA_DIR, SPEC["lora"])))206    elif SPEC["lora"]:207        adapted = apply_lora(pipe.transformer, os.path.join(MOSS_LORA_DIR, SPEC["lora"]))208    _state.update(pipe=pipe, adapted=adapted, shift=round(shift, 4) if shift else shift)209    print(f"moss-image loaded in {time.time()-t0:.1f}s | variant={MOSS_LORA or 'full'} "210          f"({SPEC['about']}) | defaults {SPEC['steps']} steps / CFG {SPEC['cfg']} / "211          f"skip {SPEC['skip']} | lora modules hooked: {adapted} | shift {_state['shift']}",212          flush=True)213214215class ImageRequest(BaseModel):216    prompt: str = Field(..., description="Text prompt, encoded by Qwen3.5-9B (token budget 4096).")217    n: int = Field(1, description="Images per prompt (num_images_per_prompt).")218    size: str = Field("1024x1024", description=f"WxH in pixels, both positive multiples of {VAE_TILE}; the pipeline rejects anything else. The fine-tune was done at 1024 px.")219    negative_prompt: str = Field("", description="Negative prompt. ONLY TAKES EFFECT when guidance_scale > 1 AND uncond_skip_layers is null/empty — by default the uncond branch is a skipped-block prediction of the POSITIVE prompt, not this text.")220    num_inference_steps: Optional[int] = Field(None, description=f"Flow-matching Euler steps. Default for this variant: {SPEC['steps']}.")221    guidance_scale: Optional[float] = Field(None, description=f"CFG scale. Default for this variant: {SPEC['cfg']}." + (" The turbo LoRAs are distilled for CFG 1; raising it is off-contract." if MOSS_LORA else ""))222    uncond_skip_layers: Optional[List[int]] = Field(None, description=f"Zero-based block indices whose skipped prediction replaces the CFG negative branch. Default for this variant: {SPEC['skip']}. Pass [] for a conventional negative branch driven by negative_prompt.")223    slg_layers: Optional[List[int]] = Field(None, description="Skip-Layer Guidance block indices. Adds a third forward per step and the correction slg_scale * (cond - cond_skip). Off by default.")224    slg_scale: float = Field(0.0, description="Skip-Layer Guidance strength. 0 disables the extra forward.")225    max_sequence_length: int = Field(4096, description="Text encoder token budget.")226    seed: Optional[int] = Field(None, description="Random seed. The initial noise is generated on CPU in float32, as the card specifies.")227    response_format: str = Field("b64_json", description="Only b64_json supported.")228229230def _params_doc():231    out = {}232    for name, info in ImageRequest.model_fields.items():233        d = info.default234        out[name] = {"default": None if d is ... else d, "description": info.description}235    out["num_inference_steps"]["resolved_default"] = SPEC["steps"]236    out["guidance_scale"]["resolved_default"] = SPEC["cfg"]237    out["uncond_skip_layers"]["resolved_default"] = SPEC["skip"]238    return out239240241@app.get("/health")242def health():243    return {"status": "ok" if _state else "degraded", "model": SERVED_MODEL_ID,244            "loaded": bool(_state), "variant": MOSS_LORA or "full",245            "lora_modules": _state.get("adapted")}246247248@app.get("/v1/models")249def models():250    return {"object": "list", "data": [{"id": SERVED_MODEL_ID, "object": "model"}]}251252253@app.get("/v1/config")254def config():255    return {"model": SERVED_MODEL_ID, "variant": MOSS_LORA or "full",256            "lora": SPEC["lora"], "params": _params_doc(),257            "scheduler": {"type": "fresh-noise (clean estimate, re-noise; ComfyUI lcm)" if SPEC.get("sampler") == "fresh-noise" else "flow-matching Euler", "shift": _state.get("shift")},258            "notes": [259                f"This variant is the {SPEC['about']}: {SPEC['steps']} steps at CFG "260                f"{SPEC['cfg']}. The three Kromoss variants do NOT share a sampler "261                "contract — send only a prompt to get the right one.",262                "uncond_skip_layers is NOT a negative-prompt control: by default the CFG "263                "uncond branch is a skipped-block prediction of the POSITIVE prompt, so "264                "negative_prompt is ignored unless you pass uncond_skip_layers: [].",265                "The LoRA is applied UNMERGED through forward hooks, as the card requires "266                "— its delta is close to bf16's rounding step, so merging would lose it."267                if SPEC["lora"] else268                "No LoRA: this is the full 50-step model. kromoss-dmd2 is the author's "269                "recommended 8-step variant and scores higher on PartiPrompts.",270                "Text encoder is Qwen3.5-9B as a pipeline component, not a served LLM, so "271                "this rig's vLLM gap on the qwen3_5 architecture does not apply.",272                "VAE is the FLUX.2 small decoder, run in float32 while the transformer and "273                "text encoder run in bfloat16, exactly as the card specifies.",274                "LICENSE: Apache-2.0, weights and bundled inference code, not gated.",275            ]}276277278@app.post("/v1/images/generations")279def generate_images(req: ImageRequest):280    if not _state:281        raise HTTPException(503, "model not loaded")282    if req.response_format != "b64_json":283        raise HTTPException(400, "only response_format=b64_json is supported")284    try:285        w, h = (int(x) for x in req.size.lower().split("x"))286    except ValueError:287        raise HTTPException(400, f"size must look like 1024x1024; got {req.size}")288    if w <= 0 or h <= 0 or w % VAE_TILE or h % VAE_TILE:289        raise HTTPException(400, f"size must be positive multiples of {VAE_TILE} in both "290                                 f"dimensions; got {req.size}")291292    steps = req.num_inference_steps if req.num_inference_steps is not None else SPEC["steps"]293    cfg = req.guidance_scale if req.guidance_scale is not None else SPEC["cfg"]294    skip = req.uncond_skip_layers if req.uncond_skip_layers is not None else SPEC["skip"]295    seed = req.seed if req.seed is not None else int(time.time() * 1000) % (2**31)296    t0 = time.time()297298    with _lock:299        queued = time.time() - t0300        # CPU generator in fp32: the card is explicit that the initial noise is fp32.301        generator = torch.Generator("cpu").manual_seed(seed)302        if SPEC.get("sampler") == "fresh-noise":303            _state["pipe"].scheduler.generator = torch.Generator("cuda").manual_seed(seed)304        try:305            out = _state["pipe"](306                req.prompt, height=h, width=w, num_inference_steps=steps,307                guidance_scale=cfg, negative_prompt=req.negative_prompt,308                num_images_per_prompt=req.n, generator=generator,309                max_sequence_length=req.max_sequence_length,310                uncond_skip_layers=skip, slg_layers=req.slg_layers,311                slg_scale=req.slg_scale)312        except ValueError as exc:313            # The pipeline validates skip-layer indices, slg bounds and the size rule.314            raise HTTPException(400, str(exc))315        images = out.images316317    data = []318    for image in images:319        buf = io.BytesIO(); image.save(buf, format="PNG")320        data.append({"b64_json": base64.b64encode(buf.getvalue()).decode()})321322    elapsed = time.time() - t0323    print(f"Generated {len(data)} image(s) {req.size} in {elapsed:.1f}s "324          f"({MOSS_LORA or 'full'}, {steps} steps, CFG {cfg}, skip {skip})", flush=True)325    return JSONResponse({326        "created": int(time.time()),327        "data": data,328        "params_used": {329            "n": req.n, "variant": MOSS_LORA or "full", "lora": SPEC["lora"],330            "size": req.size, "num_inference_steps": steps, "guidance_scale": cfg,331            "uncond_skip_layers": skip, "slg_layers": req.slg_layers,332            "slg_scale": req.slg_scale, "shift": _state.get("shift"),333            "negative_prompt": req.negative_prompt,334            "negative_prompt_effective": bool(req.negative_prompt) and cfg > 1 and not skip,335            "max_sequence_length": req.max_sequence_length, "seed": seed,336            "queued_s": round(queued, 3), "elapsed_s": round(elapsed, 3),337        },338    })339340341if __name__ == "__main__":342    print(f"{SERVED_MODEL_ID} adapter on :{PORT}", flush=True)343    _load()344    uvicorn.run(app, host="0.0.0.0", port=PORT)