Spaces:
Running
Running
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)
|