File size: 14,510 Bytes
fed954e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
"""
model_registry.py — The Qwen VLM model universe (analogous to SLOT_REGISTRY).

One ModelSpec per checkpoint family. The benchmark iterates this registry to
decide what to load on the 96GB RTX 6000 Pro. Both generations are natively
multimodal (every checkpoint has its own ViT):

  * Qwen3.5   (qwen3_5 / qwen3_5_moe, AutoModelForMultimodalLM) — `enable_thinking`
              toggle on a single checkpoint.
  * Qwen3-VL  (qwen3_vl / qwen3_vl_moe, AutoModelForImageTextToText) — separate
              -Instruct / -Thinking checkpoints.

VRAM rule of thumb (weights only): bf16 ≈ 2·B, fp8 ≈ B, int4 ≈ 0.55·B GB.
For MoE, ALL experts are resident, so total params drive memory; active params
drive decode speed (and thus throughput / fleet score).

Nothing hardcodes a repo id outside this file — it is the single source of truth
for the model ladder, exactly as registry.py is for the schema.
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Literal, Optional

Family = Literal["qwen3_5", "qwen3_5_moe", "qwen3_vl", "qwen3_vl_moe", "joycaption"]
LoaderKind = Literal["multimodal_lm", "image_text_to_text", "llava_conditional"]
ReasoningMode = Literal["toggle", "separate_ckpt", "none"]
Precision = Literal["bf16", "fp8", "int4"]
Reasoning = Literal["instruct", "thinking"]

# weights-only bytes-per-parameter, in GB-per-billion-params
_BYTES_PER_B: dict[str, float] = {"bf16": 2.0, "fp8": 1.0, "int4": 0.55}

# Headroom reserved for the vision encoder, activations, and KV cache.
VRAM_HEADROOM_GB = 12.0
GPU_VRAM_GB = 96.0


@dataclass(frozen=True)
class ModelSpec:
    key: str                         # stable short id used on the CLI + in filenames
    repo_id: str                     # the Instruct / default checkpoint
    family: Family
    params_b: float                  # total parameters (billions)
    loader_kind: LoaderKind
    reasoning_mode: ReasoningMode
    active_b: Optional[float] = None  # active params for MoE (decode speed)
    thinking_repo_id: Optional[str] = None  # separate -Thinking ckpt (Qwen3-VL)
    quant_repo_ids: dict[Precision, str] = field(default_factory=dict)  # fp8/int4 ckpts
    is_moe: bool = False
    is_baseline: bool = False        # the user's fine-tuned reference
    notes: str = ""

    # ── VRAM math ────────────────────────────────────────────────────────────

    def est_vram_gb(self, precision: Precision = "bf16") -> float:
        return self.params_b * _BYTES_PER_B[precision]

    def fits_on(self, precision: Precision = "bf16", gpu_gb: float = GPU_VRAM_GB,
                headroom: float = VRAM_HEADROOM_GB) -> bool:
        return self.est_vram_gb(precision) + headroom <= gpu_gb

    def available_precisions(self) -> list[Precision]:
        """bf16 is always available (base repo); fp8/int4 only if a quant ckpt exists."""
        return ["bf16"] + [p for p in ("fp8", "int4") if p in self.quant_repo_ids]

    def best_fitting_precision(self, gpu_gb: float = GPU_VRAM_GB) -> Optional[Precision]:
        """Smallest-footprint precision that fits, preferring higher fidelity first
        (bf16 > fp8 > int4). Returns None if nothing fits without CPU offload."""
        for prec in ("bf16", "fp8", "int4"):
            if prec in self.available_precisions() and self.fits_on(prec, gpu_gb):
                return prec
        return None

    def needs_offload(self, gpu_gb: float = GPU_VRAM_GB) -> bool:
        return self.best_fitting_precision(gpu_gb) is None

    def repo_for(self, precision: Precision, reasoning: Reasoning = "instruct") -> str:
        """Resolve the concrete HF repo id for a (precision, reasoning) request."""
        if reasoning == "thinking" and self.reasoning_mode == "separate_ckpt":
            if self.thinking_repo_id is None:
                raise ValueError(f"{self.key}: no separate thinking checkpoint")
            base = self.thinking_repo_id
        else:
            base = self.repo_id
        if precision != "bf16" and precision in self.quant_repo_ids:
            # Quant repos are published for the instruct line; thinking quants are
            # less common, so fall back to the instruct quant id if needed.
            return self.quant_repo_ids[precision]
        return base

    def supports_thinking(self) -> bool:
        return self.reasoning_mode == "toggle" or self.thinking_repo_id is not None

    def enable_thinking_flag(self, reasoning: Reasoning) -> bool:
        """For toggle models, thinking is a generation flag, not a separate repo."""
        return reasoning == "thinking" and self.reasoning_mode == "toggle"


# ──────────────────────────────────────────────────────────────────────────────
# THE MODEL UNIVERSE. Bench everything that fits on 96GB, both reasoning variants.
# Stretch rungs (397B / 235B) are kept but flagged needs_offload by the VRAM math.
# ──────────────────────────────────────────────────────────────────────────────

def _q(*pairs: tuple[Precision, str]) -> dict[Precision, str]:
    return dict(pairs)


MODEL_REGISTRY: dict[str, ModelSpec] = {
    # ── Qwen3.5 dense (native multimodal; enable_thinking toggle) ──────────────
    "qwen3.5-0.8b": ModelSpec(
        key="qwen3.5-0.8b", repo_id="Qwen/Qwen3.5-0.8B", family="qwen3_5",
        params_b=0.873, loader_kind="multimodal_lm", reasoning_mode="toggle",
        notes="smallest native VLM; floor of the ladder",
    ),
    "qwen3.5-2b": ModelSpec(
        key="qwen3.5-2b", repo_id="Qwen/Qwen3.5-2B", family="qwen3_5",
        params_b=2.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
    ),
    "qwen3.5-4b": ModelSpec(
        key="qwen3.5-4b", repo_id="Qwen/Qwen3.5-4B", family="qwen3_5",
        params_b=4.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
    ),
    "qwen3.5-9b": ModelSpec(
        key="qwen3.5-9b", repo_id="Qwen/Qwen3.5-9B", family="qwen3_5",
        params_b=9.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
    ),
    "qwen3.5-27b": ModelSpec(
        key="qwen3.5-27b", repo_id="Qwen/Qwen3.5-27B", family="qwen3_5",
        params_b=27.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3.5-27B-FP8"), ("int4", "Qwen/Qwen3.5-27B-GPTQ-Int4")),
    ),
    # ── Qwen3.5 MoE ────────────────────────────────────────────────────────────
    "qwen3.5-35b-a3b": ModelSpec(
        key="qwen3.5-35b-a3b", repo_id="Qwen/Qwen3.5-35B-A3B", family="qwen3_5_moe",
        params_b=35.0, active_b=3.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
        is_moe=True,
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3.5-35B-A3B-FP8"), ("int4", "Qwen/Qwen3.5-35B-A3B-GPTQ-Int4")),
        notes="MoE: ~3B active → decodes near a 3B dense",
    ),
    "qwen3.5-122b-a10b": ModelSpec(
        key="qwen3.5-122b-a10b", repo_id="Qwen/Qwen3.5-122B-A10B", family="qwen3_5_moe",
        params_b=122.0, active_b=10.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
        is_moe=True,
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3.5-122B-A10B-FP8"), ("int4", "Qwen/Qwen3.5-122B-A10B-GPTQ-Int4")),
        notes="fits 96GB only at GPTQ-Int4 (~67GB)",
    ),
    "qwen3.5-397b-a17b": ModelSpec(
        key="qwen3.5-397b-a17b", repo_id="Qwen/Qwen3.5-397B-A17B", family="qwen3_5_moe",
        params_b=397.0, active_b=17.0, loader_kind="multimodal_lm", reasoning_mode="toggle",
        is_moe=True,
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3.5-397B-A17B-FP8"), ("int4", "Qwen/Qwen3.5-397B-A17B-GPTQ-Int4")),
        notes="STRETCH: needs CPU offload even at Int4",
    ),
    # ── Qwen3-VL dense (separate -Instruct / -Thinking) ────────────────────────
    "qwen3vl-2b": ModelSpec(
        key="qwen3vl-2b", repo_id="Qwen/Qwen3-VL-2B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-2B-Thinking", family="qwen3_vl",
        params_b=2.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-2B-Instruct-FP8")),
    ),
    "qwen3vl-4b": ModelSpec(
        key="qwen3vl-4b", repo_id="Qwen/Qwen3-VL-4B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-4B-Thinking", family="qwen3_vl",
        params_b=4.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-4B-Instruct-FP8")),
    ),
    "qwen3vl-8b": ModelSpec(
        key="qwen3vl-8b", repo_id="Qwen/Qwen3-VL-8B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-8B-Thinking", family="qwen3_vl",
        params_b=8.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-8B-Instruct-FP8")),
    ),
    "qwen3vl-32b": ModelSpec(
        key="qwen3vl-32b", repo_id="Qwen/Qwen3-VL-32B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-32B-Thinking", family="qwen3_vl",
        params_b=32.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-32B-Instruct-FP8")),
        notes="top dense that fits bf16 (~64GB)",
    ),
    # ── Qwen3-VL MoE ───────────────────────────────────────────────────────────
    "qwen3vl-30b-a3b": ModelSpec(
        key="qwen3vl-30b-a3b", repo_id="Qwen/Qwen3-VL-30B-A3B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-30B-A3B-Thinking", family="qwen3_vl_moe",
        params_b=30.0, active_b=3.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        is_moe=True, quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-30B-A3B-Instruct-FP8")),
        notes="MoE: ~3B active, fits bf16 (~60GB)",
    ),
    "qwen3vl-235b-a22b": ModelSpec(
        key="qwen3vl-235b-a22b", repo_id="Qwen/Qwen3-VL-235B-A22B-Instruct",
        thinking_repo_id="Qwen/Qwen3-VL-235B-A22B-Thinking", family="qwen3_vl_moe",
        params_b=235.0, active_b=22.0, loader_kind="image_text_to_text", reasoning_mode="separate_ckpt",
        is_moe=True, quant_repo_ids=_q(("fp8", "Qwen/Qwen3-VL-235B-A22B-Instruct-FP8")),
        notes="STRETCH: needs CPU offload",
    ),
    # ── User's fine-tuned reference (combined ViT-classification + JSON) ────────
    "qwen3.5-0.8b-json-captioner": ModelSpec(
        key="qwen3.5-0.8b-json-captioner",
        repo_id="AbstractPhil/Qwen3.5-0.8B-json-captioner", family="qwen3_5",
        params_b=0.873, loader_kind="multimodal_lm", reasoning_mode="toggle",
        is_baseline=True,
        notes="LoRA-merged baseline: existence proof of native ViT-class + JSON; throughput ceiling",
    ),
    # ── JoyCaption (LLaVA: SigLIP2/SigLIP + Llama 3.1 8B). Captioner JSON-capacity ─
    #    baseline — not grounding-trained, so treat coordinate tasks as exploratory.
    "joycaption-beta-one": ModelSpec(
        key="joycaption-beta-one",
        repo_id="fancyfeast/llama-joycaption-beta-one-hf-llava", family="joycaption",
        params_b=8.48, loader_kind="llava_conditional", reasoning_mode="none",
        notes="SigLIP2 + Llama 3.1 8B captioner; latest JoyCaption; not grounding-trained",
    ),
    "joycaption-alpha-two": ModelSpec(
        key="joycaption-alpha-two",
        repo_id="fancyfeast/llama-joycaption-alpha-two-hf-llava", family="joycaption",
        params_b=8.48, loader_kind="llava_conditional", reasoning_mode="none",
        notes="prior JoyCaption (SigLIP v1); version-over-version JSON comparison",
    ),
}


# ──────────────────────────────────────────────────────────────────────────────
# Query helpers
# ──────────────────────────────────────────────────────────────────────────────

def get_model(key: str) -> ModelSpec:
    if key not in MODEL_REGISTRY:
        raise KeyError(f"unknown model: {key!r}. known: {list(MODEL_REGISTRY)}")
    return MODEL_REGISTRY[key]


def model_keys() -> list[str]:
    return list(MODEL_REGISTRY.keys())


def models_that_fit(gpu_gb: float = GPU_VRAM_GB, include_offload: bool = False) -> list[ModelSpec]:
    """Models that fit at some precision on `gpu_gb` (plus offload stretch if asked)."""
    out = []
    for spec in MODEL_REGISTRY.values():
        if spec.best_fitting_precision(gpu_gb) is not None:
            out.append(spec)
        elif include_offload:
            out.append(spec)
    return out


def reasoning_variants(spec: ModelSpec, both: bool = True) -> list[Reasoning]:
    """The reasoning variants to run for a model."""
    if not both or not spec.supports_thinking():
        return ["instruct"]
    return ["instruct", "thinking"]


def get_runner(model_key: str, precision: Precision = "bf16", reasoning: Reasoning = "instruct",
               device_map: Optional[str] = None, **kwargs):
    """Factory: build a real VLMRunner for a (model, precision, reasoning) request.

    Imports torch lazily (via runners), so importing this registry stays cheap.
    CPU-offload stretch rungs get device_map='auto' automatically.
    """
    from .runners import VLMRunner
    spec = get_model(model_key)
    if precision == "bf16" and not spec.fits_on("bf16") and spec.best_fitting_precision() is not None:
        precision = spec.best_fitting_precision()  # auto-downshift to a fitting quant
    repo = spec.repo_for(precision, reasoning)
    if device_map is None:
        device_map = "auto" if spec.needs_offload() else "cuda"
    return VLMRunner(
        model_id=repo,
        loader_kind=spec.loader_kind,
        precision=precision,
        enable_thinking=spec.enable_thinking_flag(reasoning),
        device_map=device_map,
        **kwargs,
    )