File size: 15,692 Bytes
938b63e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
"""What the LaunchReady Space shows: verdict, tables, wording. No Gradio, so tests run offline.

The math lives in launchready; this module picks a verdict from its numbers and writes Markdown
for a reader who isn't a developer. Everything here is projected. Text that comes from a repo
(warnings quote config values) is escaped before it reaches the page.
"""

import html
import logging
import re
from dataclasses import dataclass
from pathlib import Path
from urllib.parse import quote

from huggingface_hub.errors import HFValidationError
from huggingface_hub.utils import validate_repo_id

from launchready.hub import (
    GatedError,
    HubError,
    HubModel,
    HubUnavailableError,
    NoConfigError,
    load_model,
)
from launchready.memory import (
    BITS_PER_WEIGHT,
    GIB,
    GPU_MEMORY_UTILIZATION,
    RUNTIME_RESERVE_BYTES,
    Gpu,
    load_gpus,
)
from launchready.report import PRECISION_LABELS, FitReport, hub_file_sizes, project
from launchready.variants import Variant, load_variants

log = logging.getLogger("launchready.space")

KV_DTYPE = "fp16"
GITHUB = "https://github.com/Tanmaypathak18/launchready"
RAW = "https://raw.githubusercontent.com/Tanmaypathak18/launchready"
# Saved results, briefs and recipes don't change with the code, so links stay on this commit.
RESULTS_COMMIT = "db4e928965091bf46b99f3d8272a5b413864e718"
LIGHTNING = "nvidia/NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16"
DEFAULT_CONTEXT = 32_768
DEFAULT_USERS = 8
EXAMPLES = [
    [LIGHTNING, "rtx-5090", 32_768, 8],
    [LIGHTNING, "rtx-4090", 32_768, 8],
    ["Qwen/Qwen3-4B", "rtx-4090", 32_768, 8],
    ["deepseek-ai/DeepSeek-V3", "rtx-5090", 32_768, 8],
    ["ibm-granite/granite-4.0-h-tiny", "tesla-t4", 4_096, 8],
    ["meta-llama/Llama-3.3-70B-Instruct", "rtx-5090", 32_768, 8],
]

FITS, TOO_FEW, NO_FIT, CANT_TELL = "fits", "too few users", "does not fit", "can't tell"
MEMORY_ONLY = "This is about memory only, not speed."
HYBRID_CAUTION = (
    "**Caution:** this model is a hybrid design, and projections for hybrids can be far too high. "
    "On the one hybrid measured so far (granite-4.0-h-tiny on 2× Tesla T4), real capacity was "
    "about 43% of the projection: 118 projected, 50.6 measured. Treat the number of users here "
    "as an upper limit."
)
ENGINE_NOTE = (
    "Whether inference software supports a model isn't checked here. The `launchready brief` "
    f"command does that; see the [Nemotron 3.5 Lightning brief]({GITHUB}/blob/{RESULTS_COMMIT}/"
    f"briefs/nvidia__NVIDIA-Nemotron-3.5-Lightning-30B-A3B-BF16.md) for an example."
)
_HUB_URL = re.compile(r"^(?:https?://)?(?:www\.)?(?:huggingface\.co|hf\.co)/", re.IGNORECASE)
_MARKDOWN = re.compile(r"([\\`*_{}\[\]()#+|~!>-])")


class BadInput(ValueError):
    """Something typed into the form can't be used; the message is shown as it is."""


@dataclass
class Verdict:
    state: str
    precision: str | None = None
    max_users: int = 0


@dataclass
class Page:
    verdict: str
    memory: str = ""
    warnings: str = ""
    variants: str = ""


def esc(text: object) -> str:
    """Escape repo-derived text for Markdown and HTML."""
    return _MARKDOWN.sub(r"\\\1", html.escape(str(text), quote=False))


def gib(n: float) -> str:
    return f"{n / GIB:,.2f}"


def normalize_repo_id(text: str | None) -> str:
    """`org/name` from what was typed: an id, or a huggingface.co link to a model."""
    value = (text or "").strip().strip("/")
    if not value:
        raise BadInput("Enter a model, like `Qwen/Qwen3-4B`.")
    if _HUB_URL.match(value):
        parts = _HUB_URL.sub("", value).split("?")[0].split("#")[0].split("/")
        if parts[0] in ("datasets", "spaces", "models"):
            parts = parts[1:] if parts[0] == "models" else []
        value = "/".join(parts[:2])
    try:
        validate_repo_id(value)
    except HFValidationError as e:
        raise BadInput(
            "That doesn't look like a Hugging Face model. Use the form `org/name`, like "
            "`Qwen/Qwen3-4B`, or paste the link to the model's page."
        ) from e
    return value


def whole_number(value: float | None, name: str) -> int:
    if value is None or value < 1 or value != int(value):
        raise BadInput(f"{name} must be a whole number of at least 1.")
    return int(value)


def decide(report: FitReport, gpu: Gpu, users: int) -> Verdict:
    """The highest-quality precision this GPU supports that reaches `users`.

    Precisions the GPU doesn't support never count: their memory assumes the stored size, and an
    upcast could use more. BITS_PER_WEIGHT is ordered from highest quality down.
    """
    if not report.weights:
        return Verdict(CANT_TELL)
    fits = report.fits[gpu.id]
    supported = [p for p in BITS_PER_WEIGHT if p in fits and fits[p]["native"]]
    for p in supported:
        if fits[p]["max_users"] >= users:
            return Verdict(FITS, p, fits[p]["max_users"])
    loadable = [p for p in supported if fits[p]["max_users"] >= 1]
    if loadable:
        best = max(loadable, key=lambda p: fits[p]["max_users"])
        return Verdict(TOO_FEW, best, fits[best]["max_users"])
    return Verdict(NO_FIT, min(supported, key=lambda p: report.weights[p]))


def verdict_text(
    verdict: Verdict,
    report: FitReport,
    gpu: Gpu,
    repo_id: str,
    users: int,
    context: int,
    hub_files: dict[str, tuple[str, int]],
) -> str:
    name = esc(repo_id.split("/")[-1])
    wanted = f"{users:,} user{'s' if users != 1 else ''} at {context:,} tokens each"
    label = PRECISION_LABELS.get(verdict.precision, "")
    if verdict.state == FITS:
        lines = [
            "## Fits (projected)",
            f"{name} should fit on the **{esc(gpu.name)}** with {wanted}, using the **{label}** "
            f"version. There is room for up to {verdict.max_users:,} users.",
        ]
    elif verdict.state == TOO_FEW:
        lines = [
            f"## Does not fit {users:,} users (projected)",
            f"{name} loads on the **{esc(gpu.name)}**, but there is only room for "
            f"{verdict.max_users:,} at {context:,} tokens each, using the **{label}** version.",
        ]
    elif verdict.state == NO_FIT:
        weights = report.weights[verdict.precision]
        room = report.fits[gpu.id][verdict.precision]["kv_budget_bytes"] + weights
        lines = [
            "## Does not fit (projected)",
            f"{name} does not fit on the **{esc(gpu.name)}**. The smallest version this GPU "
            f"supports, **{label}**, needs {gib(weights)} GiB plus {gib(report.seq.total)} GiB "
            f"per user, and {gib(room)} GiB is available.",
        ]
    else:
        return "\n\n".join(
            [
                "## Can't tell",
                f"{name} has no safetensors files, so its size can't be read without downloading "
                "it. LaunchReady never downloads model weights.",
            ]
        )
    if verdict.state != NO_FIT and verdict.precision not in hub_files:
        lines.append(f"No {label} version was found on the Hub, so its size is projected.")
    if report.arch.max_context and context > report.arch.max_context:
        lines.append(f"This model's limit is {report.arch.max_context:,} tokens per conversation.")
    if report.seq.mamba.dtype is not None and verdict.state != NO_FIT:
        lines.append(HYBRID_CAUTION)
    lines.append(
        f"{MEMORY_ONLY} Projected: usable memory = GPU memory × {GPU_MEMORY_UTILIZATION:.2f} − "
        f"weights − {RUNTIME_RESERVE_BYTES // GIB} GiB; users = usable memory ÷ memory per user."
    )
    return "\n\n".join(lines)


def memory_text(
    report: FitReport,
    gpu: Gpu,
    model: HubModel,
    context: int,
    hub_files: dict[str, tuple[str, int]],
) -> str:
    lines = [
        "## Memory (projected)",
        f"{esc(model.repo_id)} @ `{model.revision[:7]}`, license "
        f"{esc(model.license or 'not declared')}.",
    ]
    if report.weights:
        lines.append(
            f"On {esc(gpu.name)} ({gpu.memory_gib:g} GiB) at {context:,} tokens per user. Figures "
            "are memory only, not speed.\n\n"
            "| Version | Bits per weight | Weights GiB (projected) | Hub file GiB | "
            "Max users (projected) | Supported by this GPU |\n|---|--:|--:|--:|--:|---|\n"
            + "\n".join(
                f"| {PRECISION_LABELS[p]} | {BITS_PER_WEIGHT[p]:g} | "
                f"{gib(report.projected_weights[p])} | "
                f"{gib(hub_files[p][1]) if p in hub_files else '—'} | "
                f"{report.fits[gpu.id][p]['max_users']:,} | "
                f"{'Yes' if report.fits[gpu.id][p]['native'] else 'No, not counted'} |"
                for p in report.weights
            )
        )
        lines.append(
            "Max users uses the Hub file size where there is one, because real files are often "
            "larger than the projection. A file size is not measured GPU memory."
        )
    per_user = f"KV cache {gib(report.seq.attention_bytes)} GiB"
    if report.seq.mamba.dtype is not None:
        per_user += (
            f" + Mamba state {gib(report.seq.mamba_bytes)} GiB = {gib(report.seq.total)} GiB"
        )
    lines.append(
        f"Memory per user at {context:,} tokens (projected, KV cache {KV_DTYPE.upper()}): "
        f"{per_user}."
    )
    return "\n\n".join(lines)


def warnings_text(report: FitReport, extra: list[str]) -> str:
    if not (report.warnings or report.notes or extra):
        return ""
    lines = ["## Technical warnings", "The same warnings the `launchready fit` command gives."]
    items = [f"- **Warning:** {esc(w)}" for w in extra + report.warnings]
    items += [f"- Note: {esc(n)}" for n in report.notes]
    return "\n\n".join(lines) + "\n\n" + "\n".join(items)


def _variant_size(v: Variant) -> str:
    if v.gguf:
        shown = [f"{q} {gib(v.gguf[q])} GiB" for q in ("Q4_K_M", "Q8_0") if q in v.gguf]
        return " · ".join(shown) or f"{len(v.gguf)} quant types"
    return f"{gib(v.total_bytes)} GiB" if v.total_bytes else "—"


def _model_link(repo_id: str, text: str | None = None, path: str = "") -> str:
    return f"[{esc(text or repo_id)}](https://huggingface.co/{quote(repo_id)}{path})"


def variants_text(model: HubModel, variants: list[Variant]) -> str:
    lines = ["## Versions and links"]
    rows = [v for v in variants if v.format != "drafter"]
    if rows:
        lines.append(
            "Other versions of this model on the Hub, with file sizes:\n\n"
            "| Format | Repo | Size | Downloads |\n|---|---|--:|--:|\n"
            + "\n".join(
                f"| {esc(v.format)} | {_model_link(v.repo_id)} | {_variant_size(v)} | "
                f"{v.downloads:,} |"
                for v in rows
            )
        )
    else:
        lines.append("No other versions were found on the Hub.")
    links = [
        _model_link(model.repo_id, "Model page", f"/tree/{model.revision}"),
        _model_link(model.repo_id, "config.json", f"/blob/{model.revision}/config.json"),
        f"[All quantized versions](https://huggingface.co/models?other="
        f"base_model:quantized:{quote(model.repo_id)})",
    ]
    lines.append(" · ".join(links))
    lines.append(ENGINE_NOTE)
    return "\n\n".join(lines)


def error_text(error: Exception, repo_id: str) -> str:
    """What to tell a reader when a repo can't be sized. Never asks for a token."""
    name = esc(repo_id)
    if isinstance(error, GatedError):
        body = (
            f"The owner of {name} requires people to accept a license first. This free tool has "
            "no login, so it can't read the model. Accept the license on "
            f"{_model_link(repo_id, 'its Hugging Face page')}, then use the `launchready` "
            f"command-line tool ([GitHub]({GITHUB})) with your own account."
        )
        return f"## License required\n\n{body}"
    if isinstance(error, NoConfigError):
        body = (
            f"{name} has no config.json, so it can't be sized. GGUF download repos usually don't "
            "have one. Try the original model's repo instead."
        )
        return f"## Can't size this repo\n\n{body}"
    if isinstance(error, HubUnavailableError):
        body = "Hugging Face didn't answer, or this free tool is being rate limited. "
        return f"## Try again in a minute\n\n{body}Your input was fine."
    if isinstance(error, HubError):
        return (
            f"## Model not found\n\n{name} wasn't found on Hugging Face. Check the spelling. "
            "Private models can't be read here."
        )
    if isinstance(error, ValueError):
        return (
            f"## Can't size this repo\n\nLaunchReady can't read the config of {name}. It may "
            f"not be a text model, or it may use an older config format. Details: {esc(error)}."
        )
    return "## Something went wrong\n\nThis model couldn't be checked. Please try another one."


def check(repo_text: str | None, gpu_id: str, context: float | None, users: float | None) -> Page:
    """The whole page for one form submission. Never raises: failures become a message."""
    repo_id = ""
    try:
        repo_id = normalize_repo_id(repo_text)
        context = whole_number(context, "Context length")
        users = whole_number(users, "Number of users")
        gpu = next(g for g in load_gpus() if g.id == gpu_id)
        model = load_model(repo_id)
        extra, variants = [], []
        try:
            variants = load_variants(repo_id)
        except Exception as e:
            log.warning("variants lookup failed: %s", type(e).__name__)
            extra.append("couldn't list other versions: weights below are projected")
        hub_files = hub_file_sizes(model, variants)
        report = project(model, context, KV_DTYPE, [gpu], hub_files)
    except BadInput as e:
        return Page(f"## Check your input\n\n{e}")
    except (HubError, ValueError) as e:
        return Page(error_text(e, repo_id))
    except Exception as e:
        log.exception("unexpected failure: %s", type(e).__name__)
        return Page(error_text(e, repo_id))
    verdict = decide(report, gpu, users)
    return Page(
        verdict=verdict_text(verdict, report, gpu, repo_id, users, context, hub_files),
        memory=memory_text(report, gpu, model, context, hub_files),
        warnings=warnings_text(report, extra),
        variants=variants_text(model, variants),
    )


def how_calculated_text(gpus: list[Gpu]) -> str:
    lines = [
        "## How this is calculated",
        "Every number above is **projected**, not measured. No model weights are downloaded: "
        "only config.json and the safetensors file headers. Figures are memory only, not speed.",
        "- **Weights** = parameters × bits per weight ÷ 8, or the Hub file size where one exists\n"
        "- **Memory per user** = KV cache per token × context length + Mamba state, if any\n"
        f"- **Usable memory** = GPU memory × {GPU_MEMORY_UTILIZATION:.2f} − weights − "
        f"{RUNTIME_RESERVE_BYTES // GIB} GiB\n"
        "- **Max users** = usable memory ÷ memory per user, rounded down",
        "GPU memory is the size on NVIDIA's spec pages read as GiB, checked 2026-11-03, except "
        "the Tesla T4, which uses the measured 15 GiB.",
    ]
    unverified = [f"- **{esc(g.name)}:** {esc(g.unverified)}" for g in gpus if g.unverified]
    if unverified:
        lines.append("Unverified GPU details:\n\n" + "\n".join(unverified))
    return "\n\n".join(lines)


def measured_text() -> str:
    text = (Path(__file__).parent / "measured.md").read_text()
    return text.replace("{GITHUB}", GITHUB).replace("{RAW}", RAW).replace("{REF}", RESULTS_COMMIT)