stevhliu HF Staff commited on
Commit
5ae37d4
·
1 Parent(s): 104da23

Build the quantization picker

Browse files

An interactive replacement for the quantization method table in the
Transformers docs. The table lists bits; the deciding arithmetic is bytes,
and that is where people stall. This does the arithmetic.

- lib/data.ts holds the methods and the memory model: weights from params ×
bits, KV cache from the attention shape, activations as a flat estimate.
- lib/hardware.ts is scraped from huggingface.co/hardware, one entry per
memory configuration (316 of them) rather than per device, since an RTX
3060 is sold as both 8GB and 12GB.
- The model is typed as a Hub repo id and resolved through /api/model, so any
model works, not a curated five.
- MemoryBar draws the selected precision solid with fp16 behind it as a
ghost, so quantizing shows its own effect.
- TradeoffMap places the matching methods by when they quantize and whether
they serialize — the axes that actually separate them, since every method
at a given precision lands at the same footprint.

Deploys to a Space via the Docker SDK.

.claude/launch.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "version": "0.0.1",
3
+ "configurations": [
4
+ {
5
+ "name": "quantization-picker",
6
+ "runtimeExecutable": "npm",
7
+ "runtimeArgs": ["run", "dev"],
8
+ "port": 3000
9
+ }
10
+ ]
11
+ }
.dockerignore ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ node_modules
2
+ .next
3
+ .git
4
+ .claude
5
+ npm-debug.log
Dockerfile ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # syntax=docker/dockerfile:1
2
+
3
+ FROM node:22-alpine AS deps
4
+ WORKDIR /app
5
+ COPY package.json package-lock.json ./
6
+ RUN npm ci
7
+
8
+ FROM node:22-alpine AS builder
9
+ WORKDIR /app
10
+ COPY --from=deps /app/node_modules ./node_modules
11
+ COPY . .
12
+ ENV NEXT_TELEMETRY_DISABLED=1
13
+ RUN npm run build
14
+
15
+ FROM node:22-alpine AS runner
16
+ WORKDIR /app
17
+ ENV NODE_ENV=production
18
+ ENV NEXT_TELEMETRY_DISABLED=1
19
+ # Spaces routes traffic to 7860 and runs the container as uid 1000.
20
+ ENV PORT=7860
21
+ ENV HOSTNAME=0.0.0.0
22
+ EXPOSE 7860
23
+
24
+ COPY --from=builder /app/public ./public
25
+ COPY --from=builder --chown=node:node /app/.next/standalone ./
26
+ COPY --from=builder --chown=node:node /app/.next/static ./.next/static
27
+
28
+ USER node
29
+ CMD ["node", "server.js"]
README.md CHANGED
@@ -1,36 +1,97 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
 
 
 
 
 
 
 
 
 
 
2
 
3
- ## Getting Started
4
 
5
- First, run the development server:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- ```bash
8
- npm run dev
9
- # or
10
- yarn dev
11
- # or
12
- pnpm dev
13
- # or
14
- bun dev
15
  ```
 
 
 
 
 
 
16
 
17
- Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
 
 
 
18
 
19
- You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
 
20
 
21
- This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
 
23
- ## Learn More
 
 
24
 
25
- To learn more about Next.js, take a look at the following resources:
 
 
 
 
 
26
 
27
- - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
- - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
- You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
 
32
- ## Deploy on Vercel
 
 
 
 
33
 
34
- The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
 
36
- Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
 
 
 
 
 
 
 
1
+ ---
2
+ title: Quantization picker
3
+ emoji: 🧮
4
+ colorFrom: gray
5
+ colorTo: gray
6
+ sdk: docker
7
+ app_port: 7860
8
+ pinned: false
9
+ license: apache-2.0
10
+ short_description: Work out what fits on your card, and which method to use
11
+ ---
12
 
13
+ # Quantization picker
14
 
15
+ An interactive version of the quantization method table from the Transformers
16
+ docs. Instead of a 22×13 grid of status emoji, it answers the two questions
17
+ people actually arrive with: **will this fit on my card**, and **which method
18
+ should I use**.
19
+
20
+ ## Screens
21
+
22
+ | Route | What it does |
23
+ | --- | --- |
24
+ | `/` | Pick a model, a card, and a goal. Shows total memory footprint against your VRAM, with every precision marked on one bar. |
25
+ | `/compare` | The methods that match, plotted by when they quantize and whether the result can be saved, plus a capability table. |
26
+
27
+ ## The memory model
28
+
29
+ Figures are computed in `lib/data.ts`, not hardcoded:
30
 
 
 
 
 
 
 
 
 
31
  ```
32
+ weights = params × bits / 8 # decimal GB
33
+ kv_cache = 2 × layers × kv_heads × head_dim × 2 × context_tokens
34
+ total = weights + kv_cache + activations
35
+ ```
36
+
37
+ Two things this makes visible that a bits-only table cannot:
38
 
39
+ - **KV cache is a fixed reservation.** It does not shrink when you quantize
40
+ weights, so it is drawn as its own block against the capacity line.
41
+ - **Fitting is not the same as being a good idea.** 8-bit often clears the line
42
+ with only a few GB spare, which will not survive a longer context.
43
 
44
+ `ACTIVATIONS_GB` is a flat 1.5 GB allowance and context is fixed at 8k. Both are
45
+ estimates — see Caveats.
46
 
47
+ ## Design
48
 
49
+ Vercel's system (`#fafafa` canvas, hairline rings, 6px radius, Geist Sans with
50
+ Geist Mono reserved for labels and metadata) with four accent colours borrowed
51
+ from Cursor used only to identify precisions:
52
 
53
+ | | |
54
+ | --- | --- |
55
+ | 2-bit | Crimson `#cf2d56` |
56
+ | 3-bit | Ember `#f54e00` |
57
+ | 4-bit | Forest `#34785c` |
58
+ | 8-bit | Amber `#c08532` |
59
 
60
+ Fit and overflow are encoded as a **tonal value shift** — solid obsidian for what
61
+ fits, graphite for the KV reservation, hairline grey for what spills — rather
62
+ than red/green, so the chart reads without relying on colour.
63
+
64
+ ## Caveats
65
+
66
+ - Capability flags mirror the current docs table, including its gaps. `?` means
67
+ genuinely unverified upstream, not "not yet filled in".
68
+ - Config snippets in the comparison table need checking against the installed
69
+ Transformers version before anyone copies them.
70
+ - Setup minutes are order-of-magnitude estimates for a 70B calibration pass.
71
+ - Quality at a given precision is deliberately **not** plotted — there is no
72
+ trustworthy per-method number to plot.
73
+
74
+ ## Running it
75
+
76
+ ```bash
77
+ npm install
78
+ npm run dev
79
+ ```
80
 
81
+ ## Hardware data
82
 
83
+ `lib/hardware.ts` is scraped from the [Hugging Face hardware
84
+ table](https://huggingface.co/hardware?view=table) — the Hardware and Memory
85
+ columns only. 316 entries, one per memory configuration rather than per device,
86
+ because an RTX 3060 is sold as both 8GB and 12GB and only one of those runs a
87
+ 7B at 8-bit. Order is the table's own, most owners first.
88
 
89
+ Two caveats:
90
 
91
+ - **CPUs are omitted.** All 59 CPU rows list `—` for memory: they draw on
92
+ system RAM, so there is no fixed pool to size a bar against.
93
+ - **`backend` is derived, not scraped.** The table has no backend column, so
94
+ vendor maps to `cuda` / `rocm` / `metal` / `intel`. That is coarse — a GTX
95
+ 1060 and a B300 are both `cuda` — so the picker will offer a method on
96
+ hardware too old to run it. Method filtering by compute capability, Metal
97
+ kernel availability, and ROCm version needs a source the table does not have.
app/api/model/route.ts ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Model } from "@/lib/data";
2
+
3
+ const HUB = "https://huggingface.co";
4
+
5
+ interface HubModel {
6
+ id: string;
7
+ gated?: false | "auto" | "manual";
8
+ safetensors?: { total?: number };
9
+ }
10
+
11
+ interface HubConfig {
12
+ num_hidden_layers?: number;
13
+ num_key_value_heads?: number;
14
+ num_attention_heads?: number;
15
+ hidden_size?: number;
16
+ head_dim?: number;
17
+ /** GPT-2 era naming, still used by a long tail of repos */
18
+ n_layer?: number;
19
+ n_head?: number;
20
+ n_embd?: number;
21
+ /** some multimodal configs nest the language model */
22
+ text_config?: HubConfig;
23
+ }
24
+
25
+ function fail(message: string, status: number) {
26
+ return Response.json({ error: message }, { status });
27
+ }
28
+
29
+ /**
30
+ * Resolve a Hub repo id into the four numbers the memory math needs.
31
+ *
32
+ * Parameter count comes from the safetensors index the Hub already computed;
33
+ * the attention shape comes from config.json. Both are public files, so no
34
+ * token is involved — which is also why gated repos cannot be sized here.
35
+ */
36
+ export async function GET(request: Request) {
37
+ const id = new URL(request.url).searchParams.get("id")?.trim();
38
+ if (!id) return fail("No model id.", 400);
39
+ if (!/^[\w.-]+\/[\w.-]+$/.test(id)) return fail("Expected owner/model.", 400);
40
+
41
+ const infoRes = await fetch(`${HUB}/api/models/${id}`, {
42
+ next: { revalidate: 3600 },
43
+ });
44
+ // The Hub answers 401 for both private and non-existent repos.
45
+ if ([401, 403, 404].includes(infoRes.status)) {
46
+ return fail(`No model called ${id}, or it is private.`, 404);
47
+ }
48
+ if (!infoRes.ok) return fail("The Hub did not answer.", 502);
49
+
50
+ const info = (await infoRes.json()) as HubModel;
51
+ const params = info.safetensors?.total;
52
+ if (!params) {
53
+ return fail("No safetensors weights, so there is nothing to size.", 422);
54
+ }
55
+
56
+ const configRes = await fetch(`${HUB}/${id}/resolve/main/config.json`, {
57
+ next: { revalidate: 3600 },
58
+ });
59
+ if (!configRes.ok) {
60
+ return fail(
61
+ info.gated
62
+ ? "This repo is gated — accept its terms on the Hub, or use an ungated mirror."
63
+ : "No config.json, so the KV cache cannot be computed.",
64
+ 422,
65
+ );
66
+ }
67
+
68
+ let config: HubConfig;
69
+ try {
70
+ config = (await configRes.json()) as HubConfig;
71
+ } catch {
72
+ return fail("config.json is not readable.", 422);
73
+ }
74
+
75
+ // Multimodal repos put the transformer under text_config.
76
+ const shape = config.text_config ?? config;
77
+ const layers = shape.num_hidden_layers ?? shape.n_layer;
78
+ const heads = shape.num_attention_heads ?? shape.n_head;
79
+ const hidden = shape.hidden_size ?? shape.n_embd;
80
+ // Multi-head attention is the degenerate case of grouped-query attention.
81
+ const kvHeads = shape.num_key_value_heads ?? heads;
82
+ // Most configs leave head_dim implicit at hidden_size / heads.
83
+ const headDim =
84
+ shape.head_dim ?? (hidden && heads ? hidden / heads : undefined);
85
+
86
+ if (!layers || !kvHeads || !headDim) {
87
+ return fail("config.json does not describe a transformer we can size.", 422);
88
+ }
89
+
90
+ const model: Model = {
91
+ id: info.id,
92
+ label: info.id.split("/").pop() ?? info.id,
93
+ params: params / 1e9,
94
+ layers,
95
+ kvHeads,
96
+ headDim,
97
+ };
98
+ return Response.json(model);
99
+ }
app/compare/page.tsx ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import Link from "next/link";
2
+ import TradeoffMap from "@/components/TradeoffMap";
3
+ import CapabilityRow from "@/components/CapabilityRow";
4
+ import {
5
+ GOALS,
6
+ GPUS,
7
+ methodsFor,
8
+ round1,
9
+ } from "@/lib/data";
10
+
11
+ export default async function ComparePage({ searchParams }: PageProps<"/compare">) {
12
+ const sp = await searchParams;
13
+ const first = (v: string | string[] | undefined, fallback: string) =>
14
+ (Array.isArray(v) ? v[0] : v) ?? fallback;
15
+
16
+ const bits = Number(first(sp.bits, "4"));
17
+ const gpu = GPUS.find((g) => g.id === first(sp.gpu, "a100-80gb")) ?? GPUS[0];
18
+ const goal = GOALS.find((g) => g.id === first(sp.goal, "serve")) ?? GOALS[0];
19
+ const methods = methodsFor(bits, gpu.backend, goal);
20
+ // The calculator knows the footprint; it arrives as ?gb= rather than being
21
+ // recomputed here, since the model is now whatever the user typed.
22
+ const total = Number(first(sp.gb, ""));
23
+
24
+ return (
25
+ <main className="mx-auto w-full max-w-[1280px] px-10 pb-24 pt-16">
26
+ <Link href="/" className="inline-flex items-center gap-2 text-sm text-stone hover:text-obsidian">
27
+ <svg width="12" height="10" viewBox="0 0 12 10" fill="none" aria-hidden>
28
+ <path d="M5 1L1 5L5 9M1 5H11" stroke="currentColor" strokeWidth="1.4"
29
+ strokeLinecap="round" strokeLinejoin="round" />
30
+ </svg>
31
+ Back to the calculator
32
+ </Link>
33
+
34
+ <h1 className="pt-4 text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-obsidian">
35
+ {methods.length} {methods.length === 1 ? "method quantizes" : "methods quantize"} to {bits}-bit on {gpu.label}
36
+ </h1>
37
+ <p className="max-w-[760px] pt-4 text-base leading-6 text-charcoal">
38
+ {methods.length > 0 ? (
39
+ <>
40
+ {Number.isFinite(total) && total > 0
41
+ ? `All of them land at ${round1(total)} GB. `
42
+ : "All of them land at the same footprint. "}
43
+ What separates them is what they let you do.
44
+ </>
45
+ ) : (
46
+ <>Nothing in the table supports that combination. Try a different precision or card.</>
47
+ )}
48
+ </p>
49
+
50
+ {methods.length > 0 && (
51
+ <>
52
+ <div className="pt-10">
53
+ <TradeoffMap methods={methods} />
54
+ </div>
55
+
56
+ <div className="pt-12">
57
+ <div className="flex items-center gap-6 border-b border-obsidian pb-3">
58
+ <span className="label w-[420px] shrink-0 text-stone">Method</span>
59
+ <span className="label w-[140px] shrink-0 text-center text-stone">On the fly</span>
60
+ <span className="label w-[140px] shrink-0 text-center text-stone">torch.compile</span>
61
+ <span className="label w-[140px] shrink-0 text-center text-stone">PEFT</span>
62
+ <span className="label w-[140px] shrink-0 text-center text-stone">Serializable</span>
63
+ </div>
64
+ {methods.map((m) => (
65
+ <CapabilityRow key={m.id} method={m} />
66
+ ))}
67
+ </div>
68
+
69
+ <div className="flex flex-wrap items-center gap-6 pt-8">
70
+ <Legend color="#34785c" filled label="Supported" />
71
+ <Legend color="#c08532" label="Partial" />
72
+ <span className="flex items-center gap-2">
73
+ <span className="h-[1.5px] w-[11px] shrink-0 bg-ash" />
74
+ <span className="label text-stone">Not supported</span>
75
+ </span>
76
+ <span className="flex items-center gap-2">
77
+ <span className="w-[11px] shrink-0 text-center font-mono text-xs text-smoke">?</span>
78
+ <span className="label text-stone">Unverified</span>
79
+ </span>
80
+ </div>
81
+ </>
82
+ )}
83
+ </main>
84
+ );
85
+ }
86
+
87
+ function Legend({ color, filled, label }: { color: string; filled?: boolean; label: string }) {
88
+ return (
89
+ <span className="flex items-center gap-2">
90
+ <span
91
+ className="h-[9px] w-[9px] shrink-0 rounded-full"
92
+ style={filled ? { background: color } : { boxShadow: `inset 0 0 0 1.5px ${color}` }}
93
+ />
94
+ <span className="label text-stone">{label}</span>
95
+ </span>
96
+ );
97
+ }
app/globals.css CHANGED
@@ -1,26 +1,176 @@
1
  @import "tailwindcss";
 
 
2
 
3
- :root {
4
- --background: #ffffff;
5
- --foreground: #171717;
6
- }
7
 
8
  @theme inline {
9
- --color-background: var(--background);
10
- --color-foreground: var(--foreground);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
  --font-sans: var(--font-geist-sans);
12
  --font-mono: var(--font-geist-mono);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  }
14
 
15
- @media (prefers-color-scheme: dark) {
16
- :root {
17
- --background: #0a0a0a;
18
- --foreground: #ededed;
19
- }
20
  }
21
 
22
  body {
23
- background: var(--background);
24
- color: var(--foreground);
25
- font-family: Arial, Helvetica, sans-serif;
 
 
 
 
 
 
 
26
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
 
5
+ @custom-variant dark (&:is(.dark *));
 
 
 
6
 
7
  @theme inline {
8
+ /* Vercel neutrals */
9
+ --color-canvas: #fafafa;
10
+ --color-paper: #ffffff;
11
+ --color-hairline: #ebebeb;
12
+ --color-ash: #c9c9c9;
13
+ --color-smoke: #a8a8a8;
14
+ --color-graphite: #8f8f8f;
15
+ --color-stone: #666666;
16
+ --color-charcoal: #4d4d4d;
17
+ --color-obsidian: #171717;
18
+
19
+ /* Cursor accents, used only to identify precisions */
20
+ --color-crimson: #cf2d56;
21
+ --color-ember: #f54e00;
22
+ --color-forest: #34785c;
23
+ --color-amber: #c08532;
24
+
25
  --font-sans: var(--font-geist-sans);
26
  --font-mono: var(--font-geist-mono);
27
+ --font-heading: var(--font-sans);
28
+ --color-sidebar-ring: var(--sidebar-ring);
29
+ --color-sidebar-border: var(--sidebar-border);
30
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
31
+ --color-sidebar-accent: var(--sidebar-accent);
32
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
33
+ --color-sidebar-primary: var(--sidebar-primary);
34
+ --color-sidebar-foreground: var(--sidebar-foreground);
35
+ --color-sidebar: var(--sidebar);
36
+ --color-chart-5: var(--chart-5);
37
+ --color-chart-4: var(--chart-4);
38
+ --color-chart-3: var(--chart-3);
39
+ --color-chart-2: var(--chart-2);
40
+ --color-chart-1: var(--chart-1);
41
+ --color-ring: var(--ring);
42
+ --color-input: var(--input);
43
+ --color-border: var(--border);
44
+ --color-destructive: var(--destructive);
45
+ --color-accent-foreground: var(--accent-foreground);
46
+ --color-accent: var(--accent);
47
+ --color-muted-foreground: var(--muted-foreground);
48
+ --color-muted: var(--muted);
49
+ --color-secondary-foreground: var(--secondary-foreground);
50
+ --color-secondary: var(--secondary);
51
+ --color-primary-foreground: var(--primary-foreground);
52
+ --color-primary: var(--primary);
53
+ --color-popover-foreground: var(--popover-foreground);
54
+ --color-popover: var(--popover);
55
+ --color-card-foreground: var(--card-foreground);
56
+ --color-card: var(--card);
57
+ --color-foreground: var(--foreground);
58
+ --color-background: var(--background);
59
+ --radius-sm: calc(var(--radius) * 0.6);
60
+ --radius-md: calc(var(--radius) * 0.8);
61
+ --radius-lg: var(--radius);
62
+ --radius-xl: calc(var(--radius) * 1.4);
63
+ --radius-2xl: calc(var(--radius) * 1.8);
64
+ --radius-3xl: calc(var(--radius) * 2.2);
65
+ --radius-4xl: calc(var(--radius) * 2.6);
66
  }
67
 
68
+ html,
69
+ body {
70
+ background: var(--color-canvas);
71
+ color: var(--color-obsidian);
 
72
  }
73
 
74
  body {
75
+ font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
76
+ -webkit-font-smoothing: antialiased;
77
+ }
78
+
79
+ /* Uppercase mono label — the system reserves Geist Mono for metadata */
80
+ .label {
81
+ font-family: var(--font-geist-mono), ui-monospace, monospace;
82
+ font-size: 11px;
83
+ letter-spacing: 0.071em;
84
+ text-transform: uppercase;
85
  }
86
+
87
+ /* Hairline ring that survives any background, per the Vercel spec */
88
+ .ring-hairline {
89
+ box-shadow: 0 0 0 1px var(--color-hairline);
90
+ }
91
+ .ring-raised {
92
+ box-shadow:
93
+ 0 0 0 1px rgb(0 0 0 / 0.08),
94
+ 0 0 0 2px var(--color-canvas);
95
+ }
96
+
97
+ :root {
98
+ --background: oklch(1 0 0);
99
+ --foreground: oklch(0.145 0 0);
100
+ --card: oklch(1 0 0);
101
+ --card-foreground: oklch(0.145 0 0);
102
+ --popover: oklch(1 0 0);
103
+ --popover-foreground: oklch(0.145 0 0);
104
+ --primary: oklch(0.205 0 0);
105
+ --primary-foreground: oklch(0.985 0 0);
106
+ --secondary: oklch(0.97 0 0);
107
+ --secondary-foreground: oklch(0.205 0 0);
108
+ --muted: oklch(0.97 0 0);
109
+ --muted-foreground: oklch(0.556 0 0);
110
+ --accent: oklch(0.97 0 0);
111
+ --accent-foreground: oklch(0.205 0 0);
112
+ --destructive: oklch(0.577 0.245 27.325);
113
+ --border: oklch(0.922 0 0);
114
+ --input: oklch(0.922 0 0);
115
+ --ring: oklch(0.708 0 0);
116
+ --chart-1: oklch(0.87 0 0);
117
+ --chart-2: oklch(0.556 0 0);
118
+ --chart-3: oklch(0.439 0 0);
119
+ --chart-4: oklch(0.371 0 0);
120
+ --chart-5: oklch(0.269 0 0);
121
+ --radius: 0.625rem;
122
+ --sidebar: oklch(0.985 0 0);
123
+ --sidebar-foreground: oklch(0.145 0 0);
124
+ --sidebar-primary: oklch(0.205 0 0);
125
+ --sidebar-primary-foreground: oklch(0.985 0 0);
126
+ --sidebar-accent: oklch(0.97 0 0);
127
+ --sidebar-accent-foreground: oklch(0.205 0 0);
128
+ --sidebar-border: oklch(0.922 0 0);
129
+ --sidebar-ring: oklch(0.708 0 0);
130
+ }
131
+
132
+ .dark {
133
+ --background: oklch(0.145 0 0);
134
+ --foreground: oklch(0.985 0 0);
135
+ --card: oklch(0.205 0 0);
136
+ --card-foreground: oklch(0.985 0 0);
137
+ --popover: oklch(0.205 0 0);
138
+ --popover-foreground: oklch(0.985 0 0);
139
+ --primary: oklch(0.922 0 0);
140
+ --primary-foreground: oklch(0.205 0 0);
141
+ --secondary: oklch(0.269 0 0);
142
+ --secondary-foreground: oklch(0.985 0 0);
143
+ --muted: oklch(0.269 0 0);
144
+ --muted-foreground: oklch(0.708 0 0);
145
+ --accent: oklch(0.269 0 0);
146
+ --accent-foreground: oklch(0.985 0 0);
147
+ --destructive: oklch(0.704 0.191 22.216);
148
+ --border: oklch(1 0 0 / 10%);
149
+ --input: oklch(1 0 0 / 15%);
150
+ --ring: oklch(0.556 0 0);
151
+ --chart-1: oklch(0.87 0 0);
152
+ --chart-2: oklch(0.556 0 0);
153
+ --chart-3: oklch(0.439 0 0);
154
+ --chart-4: oklch(0.371 0 0);
155
+ --chart-5: oklch(0.269 0 0);
156
+ --sidebar: oklch(0.205 0 0);
157
+ --sidebar-foreground: oklch(0.985 0 0);
158
+ --sidebar-primary: oklch(0.488 0.243 264.376);
159
+ --sidebar-primary-foreground: oklch(0.985 0 0);
160
+ --sidebar-accent: oklch(0.269 0 0);
161
+ --sidebar-accent-foreground: oklch(0.985 0 0);
162
+ --sidebar-border: oklch(1 0 0 / 10%);
163
+ --sidebar-ring: oklch(0.556 0 0);
164
+ }
165
+
166
+ @layer base {
167
+ * {
168
+ @apply border-border outline-ring/50;
169
+ }
170
+ body {
171
+ @apply bg-background text-foreground;
172
+ }
173
+ html {
174
+ @apply font-sans;
175
+ }
176
+ }
app/layout.tsx CHANGED
@@ -2,28 +2,19 @@ import type { Metadata } from "next";
2
  import { Geist, Geist_Mono } from "next/font/google";
3
  import "./globals.css";
4
 
5
- const geistSans = Geist({
6
- variable: "--font-geist-sans",
7
- subsets: ["latin"],
8
- });
9
-
10
- const geistMono = Geist_Mono({
11
- variable: "--font-geist-mono",
12
- subsets: ["latin"],
13
- });
14
 
15
  export const metadata: Metadata = {
16
- title: "Create Next App",
17
- description: "Generated by create next app",
 
18
  };
19
 
20
  export default function RootLayout({ children }: LayoutProps<"/">) {
21
  return (
22
- <html
23
- lang="en"
24
- className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
25
- >
26
- <body className="min-h-full flex flex-col">{children}</body>
27
  </html>
28
  );
29
  }
 
2
  import { Geist, Geist_Mono } from "next/font/google";
3
  import "./globals.css";
4
 
5
+ const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
6
+ const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
 
 
 
 
 
 
 
7
 
8
  export const metadata: Metadata = {
9
+ title: "Quantization picker",
10
+ description:
11
+ "Work out what fits on your card, and which quantization method to use.",
12
  };
13
 
14
  export default function RootLayout({ children }: LayoutProps<"/">) {
15
  return (
16
+ <html lang="en" className={`${geistSans.variable} ${geistMono.variable} h-full`}>
17
+ <body className="min-h-full">{children}</body>
 
 
 
18
  </html>
19
  );
20
  }
app/page.tsx CHANGED
@@ -1,69 +1,78 @@
1
- import Image from "next/image";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
 
3
- export default function Home() {
4
  return (
5
- <div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
6
- <main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
7
- <Image
8
- className="dark:invert h-5 w-[100px]"
9
- src="/next.svg"
10
- alt="Next.js logo"
11
- width={100}
12
- height={20}
13
- priority
 
 
 
 
 
 
 
14
  />
15
- <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
16
- <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
17
- To get started, edit the{" "}
18
- <code className="rounded bg-black/[.06] px-1.5 py-0.5 font-mono text-[0.9em] dark:bg-white/[.08]">
19
- page.tsx
20
- </code>{" "}
21
- file.
22
- </h1>
23
- <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
24
- Looking for a starting point or more instructions? Head over to{" "}
25
- <a
26
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
27
- className="font-medium text-zinc-950 dark:text-zinc-50"
28
- >
29
- Templates
30
- </a>{" "}
31
- or the{" "}
32
- <a
33
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
34
- className="font-medium text-zinc-950 dark:text-zinc-50"
35
- >
36
- Learning
37
- </a>{" "}
38
- center.
39
- </p>
40
- </div>
41
- <div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
42
- <a
43
- className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
44
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
45
- target="_blank"
46
- rel="noopener noreferrer"
47
- >
48
- <Image
49
- className="dark:invert h-[14px] w-4"
50
- src="/vercel.svg"
51
- alt="Vercel logomark"
52
- width={16}
53
- height={14}
54
  />
55
- Deploy Now
56
- </a>
57
- <a
58
- className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
59
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
60
- target="_blank"
61
- rel="noopener noreferrer"
62
- >
63
- Documentation
64
- </a>
65
- </div>
66
- </main>
67
- </div>
 
68
  );
69
  }
 
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import SentenceSelect from "@/components/SentenceSelect";
5
+ import SentenceCombobox from "@/components/SentenceCombobox";
6
+ import ModelInput from "@/components/ModelInput";
7
+ import MemoryBar from "@/components/MemoryBar";
8
+ import TradeoffMap from "@/components/TradeoffMap";
9
+ import { GOALS, GPUS, PRECISIONS, methodsFor, type Model } from "@/lib/data";
10
+
11
+ export default function CalculatorPage() {
12
+ const [model, setModel] = useState<Model | null>(null);
13
+ const [gpuId, setGpuId] = useState("a100-80gb");
14
+ const [bits, setBits] = useState(4);
15
+
16
+ const gpu = GPUS.find((g) => g.id === gpuId)!;
17
+ const contextTokens = 8192;
18
+ // No goal filter on this screen — every method that can hit this precision
19
+ // on this backend belongs on the map.
20
+ const methods = methodsFor(bits, gpu.backend, GOALS[0]);
21
 
 
22
  return (
23
+ <main className="mx-auto w-full max-w-[1280px] px-10 pb-24 pt-20">
24
+ <div className="flex flex-wrap items-center gap-2.5">
25
+ <span className="text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-stone">
26
+ I want to run
27
+ </span>
28
+ <ModelInput onResolve={setModel} />
29
+ <span className="text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-stone">
30
+ on
31
+ </span>
32
+ <SentenceCombobox
33
+ ariaLabel="Hardware"
34
+ placeholder="Search 316 configurations"
35
+ muted={!model}
36
+ value={gpuId}
37
+ options={GPUS.map((g) => ({ id: g.id, label: g.label }))}
38
+ onChange={setGpuId}
39
  />
40
+ </div>
41
+
42
+ <div className="pt-11">
43
+ <MemoryBar
44
+ model={model}
45
+ gpu={gpu}
46
+ contextTokens={contextTokens}
47
+ selectedBits={bits}
48
+ />
49
+ </div>
50
+
51
+ {model && (
52
+ <>
53
+ <div className="flex flex-wrap items-center gap-2.5 pt-10">
54
+ <span className="text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-stone">
55
+ Quantize to
56
+ </span>
57
+ <SentenceSelect
58
+ ariaLabel="Precision"
59
+ value={String(bits)}
60
+ options={PRECISIONS.map((p) => ({ id: String(p), label: `${p}-bit` }))}
61
+ onChange={(v) => setBits(Number(v))}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  />
63
+ </div>
64
+
65
+ {methods.length > 0 ? (
66
+ <div className="pt-8">
67
+ <TradeoffMap methods={methods} />
68
+ </div>
69
+ ) : (
70
+ <p className="label pt-5 text-graphite">
71
+ No method supports {bits}-bit on {gpu.label}
72
+ </p>
73
+ )}
74
+ </>
75
+ )}
76
+ </main>
77
  );
78
  }
components.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "base-nova",
4
+ "rsc": true,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "app/globals.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "rtl": false,
15
+ "aliases": {
16
+ "components": "@/components",
17
+ "utils": "@/lib/utils",
18
+ "ui": "@/components/ui",
19
+ "lib": "@/lib",
20
+ "hooks": "@/hooks"
21
+ },
22
+ "menuColor": "default",
23
+ "menuAccent": "subtle",
24
+ "registries": {}
25
+ }
components/CapabilityRow.tsx ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Method, Support } from "@/lib/data";
2
+
3
+ function Mark({ value }: { value: Support }) {
4
+ if (value === "yes") return <span className="h-[9px] w-[9px] rounded-full bg-forest" />;
5
+ if (value === "partial")
6
+ return <span className="h-[9px] w-[9px] rounded-full shadow-[inset_0_0_0_1.5px_#c08532]" />;
7
+ if (value === "unknown") return <span className="font-mono text-[13px] text-smoke">?</span>;
8
+ return <span className="h-[1.5px] w-[11px] bg-ash" />;
9
+ }
10
+
11
+ export default function CapabilityRow({ method }: { method: Method }) {
12
+ const allFour =
13
+ method.onTheFly &&
14
+ method.compile === "yes" &&
15
+ method.peft === "yes" &&
16
+ method.serializable === "yes";
17
+
18
+ return (
19
+ <div className="flex items-center gap-6 border-b border-hairline px-3 py-[18px]">
20
+ <div className="flex w-[420px] shrink-0 flex-col gap-1">
21
+ <div className="flex items-center gap-2.5">
22
+ <a href={method.docs} target="_blank" rel="noreferrer"
23
+ className="font-mono text-[15px] font-medium text-obsidian hover:text-forest">
24
+ {method.name}
25
+ </a>
26
+ {allFour && (
27
+ <span className="label rounded-md px-2 py-0.5 text-[10px] text-forest shadow-[0_0_0_1px_#34785c]">
28
+ All four
29
+ </span>
30
+ )}
31
+ </div>
32
+ <p className="text-[13px] text-charcoal">{method.blurb}</p>
33
+ <code className="font-mono text-[11px] text-stone">{method.config}</code>
34
+ </div>
35
+ {[method.onTheFly ? "yes" : "no", method.compile, method.peft, method.serializable].map(
36
+ (v, i) => (
37
+ <div key={i} className="flex w-[140px] shrink-0 justify-center">
38
+ <Mark value={v as Support} />
39
+ </div>
40
+ ),
41
+ )}
42
+ </div>
43
+ );
44
+ }
components/MemoryBar.tsx ADDED
@@ -0,0 +1,228 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ Model,
5
+ Gpu,
6
+ PRECISIONS,
7
+ PRECISION_ACCENT,
8
+ overheadGB,
9
+ round1,
10
+ weightsGB,
11
+ } from "@/lib/data";
12
+
13
+ const W = 1180;
14
+ const BAR_H = 55;
15
+ const R = 5.5;
16
+
17
+ interface Props {
18
+ /** null until a model resolves — the bar renders empty, but to scale */
19
+ model: Model | null;
20
+ gpu: Gpu;
21
+ contextTokens: number;
22
+ selectedBits: number;
23
+ }
24
+
25
+ interface Tick {
26
+ bits: number;
27
+ total: number;
28
+ x: number;
29
+ row: 0 | 1;
30
+ }
31
+
32
+ /** Round a scale ceiling up to something a human would pick. */
33
+ function niceCeil(v: number): number {
34
+ const step = v > 400 ? 100 : v > 200 ? 50 : v > 60 ? 10 : 5;
35
+ return Math.ceil(v / step) * step;
36
+ }
37
+
38
+ export default function MemoryBar({ model, gpu, contextTokens, selectedBits }: Props) {
39
+ if (!model) return <EmptyBar />;
40
+ return (
41
+ <FilledBar
42
+ model={model}
43
+ gpu={gpu}
44
+ contextTokens={contextTokens}
45
+ selectedBits={selectedBits}
46
+ />
47
+ );
48
+ }
49
+
50
+ /**
51
+ * The card, waiting. Just the empty track, holding the same space the filled
52
+ * bar will take, so nothing moves when a model arrives.
53
+ */
54
+ function EmptyBar() {
55
+ const barTop = 84;
56
+ const barBottom = barTop + BAR_H;
57
+
58
+ return (
59
+ <svg viewBox={`0 0 ${W} ${barBottom + 66}`} className="h-auto w-full" role="img"
60
+ aria-label="No model chosen yet">
61
+ <rect x="0.5" y={barTop + 0.5} width={W - 1} height={BAR_H} rx={R} fill="#ffffff"
62
+ stroke="#ebebeb" />
63
+ </svg>
64
+ );
65
+ }
66
+
67
+ function FilledBar({ model, gpu, contextTokens, selectedBits }: Props & { model: Model }) {
68
+ const overhead = overheadGB(model, contextTokens);
69
+ const vram = gpu.vram;
70
+
71
+ // The solid bar is the precision you picked. fp16 trails behind it as a
72
+ // ghost, so you can see what quantizing bought you.
73
+ const selWeights = weightsGB(model, selectedBits);
74
+ const selTotal = selWeights + overhead;
75
+ const ghostTotal = weightsGB(model, 16) + overhead;
76
+
77
+ // The scale follows the selection, so the bar always fills the frame. fp16
78
+ // is free to run off the end.
79
+ const scaleMax = niceCeil(Math.max(selTotal, vram) * 1.02);
80
+ const x = (gb: number) => (gb / scaleMax) * W;
81
+
82
+ const overflows = selTotal > vram;
83
+ // When it overflows, the card is full: weights fill whatever the reservation leaves.
84
+ const weightsThatFit = Math.max(0, vram - overhead);
85
+ const kvStart = overflows ? x(weightsThatFit) : x(selWeights);
86
+ const kvEnd = overflows ? x(vram) : x(selTotal);
87
+ const barEnd = x(selTotal);
88
+ const ghostEnd = x(ghostTotal);
89
+ const ghostClipped = ghostEnd > W;
90
+ const ghostRight = Math.min(ghostEnd, W - 0.5);
91
+ // The callout needs ~215px; right-align it on the block when it would run off.
92
+ const kvLabelFlips = kvStart + 215 > W;
93
+ // Two totals this close would overprint.
94
+ const showGhostLabel = ghostRight - barEnd >= 64;
95
+
96
+ // Precision ticks, staggered onto a second row when they would collide.
97
+ const MIN_GAP = 68;
98
+ const lastX: [number, number] = [-Infinity, -Infinity];
99
+ const ticks: Tick[] = PRECISIONS.map((bits) => {
100
+ const total = weightsGB(model, bits) + overhead;
101
+ return { bits, total, x: x(total), row: 0 as 0 | 1 };
102
+ })
103
+ .filter((t) => t.total <= scaleMax)
104
+ .sort((a, b) => a.x - b.x)
105
+ .map((t) => {
106
+ const row: 0 | 1 = t.x - lastX[0] >= MIN_GAP ? 0 : 1;
107
+ lastX[row] = t.x;
108
+ return { ...t, row };
109
+ });
110
+
111
+ const staggered = ticks.some((t) => t.row === 1);
112
+ const barTop = staggered ? 100 : 84;
113
+ const barBottom = barTop + BAR_H;
114
+ const height = barBottom + 66;
115
+
116
+ return (
117
+ <svg viewBox={`0 0 ${W} ${height}`} className="h-auto w-full" role="img"
118
+ aria-label={`${model.label} at ${selectedBits}-bit needs ${round1(selTotal)} GB of the ${vram} GB on ${gpu.label}`}>
119
+ {/* precision labels + drop lines above the bar */}
120
+ {ticks.map((t) => {
121
+ const accent = PRECISION_ACCENT[t.bits];
122
+ const isSel = t.bits === selectedBits;
123
+ const nameY = t.row === 0 ? 11 : 53;
124
+ const valY = t.row === 0 ? 29 : 71;
125
+ const lineTop = t.row === 0 ? 40 : 82;
126
+ return (
127
+ <g key={t.bits}>
128
+ <text x={t.x} y={nameY} textAnchor="middle" fontFamily="var(--font-geist-mono)"
129
+ fontSize="11" letterSpacing="0.79" fill={accent}>
130
+ {t.bits}-BIT
131
+ </text>
132
+ <text x={t.x} y={valY} textAnchor="middle" fontFamily="var(--font-geist-mono)"
133
+ fontSize="13" fontWeight={isSel ? 500 : 400} fill={isSel ? accent : "#171717"}>
134
+ {round1(t.total)}
135
+ </text>
136
+ <line x1={t.x} y1={lineTop} x2={t.x} y2={barTop} stroke={accent}
137
+ strokeWidth={isSel ? 2.5 : 1.75} strokeLinecap="round"
138
+ strokeDasharray={isSel ? "0.01 4" : "0.01 3.5"}
139
+ opacity={isSel ? 1 : 0.9} />
140
+ </g>
141
+ );
142
+ })}
143
+
144
+ {/* KV reservation callout — under the bar, anchored on the block it describes */}
145
+ <text
146
+ x={kvLabelFlips ? kvEnd : kvStart}
147
+ y={barBottom + 52}
148
+ textAnchor={kvLabelFlips ? "end" : "start"}
149
+ fontFamily="var(--font-geist-mono)"
150
+ fontSize="11"
151
+ letterSpacing="0.79"
152
+ fill="#8f8f8f"
153
+ >
154
+ <tspan fontSize="12" letterSpacing="0" fill="#666666">{round1(overhead)}</tspan>
155
+ {" KV CACHE + ACTIVATIONS"}
156
+ </text>
157
+
158
+ {/* track */}
159
+ <rect x="0.5" y={barTop + 0.5} width={W - 1} height={BAR_H} rx={R} fill="#ffffff"
160
+ stroke="#ebebeb" />
161
+
162
+ {/* fp16, unquantized — the ghost the solid bar is measured against. It is
163
+ left open on the right when it runs past the edge of the scale. */}
164
+ <path
165
+ d={
166
+ ghostClipped
167
+ ? `M${ghostRight} ${barTop + 0.5} H${R} A${R} ${R} 0 0 0 0.5 ${barTop + R} V${barBottom - R} A${R} ${R} 0 0 0 ${R} ${barBottom + 0.5} H${ghostRight}`
168
+ : `M${ghostRight} ${barTop + 0.5} H${R} A${R} ${R} 0 0 0 0.5 ${barTop + R} V${barBottom - R} A${R} ${R} 0 0 0 ${R} ${barBottom + 0.5} H${ghostRight} Z`
169
+ }
170
+ fill="none"
171
+ stroke="#d4d4d4"
172
+ strokeWidth="1"
173
+ strokeDasharray="2 3"
174
+ />
175
+ {showGhostLabel && (
176
+ <text x={ghostRight - 6} y={barTop - 8} textAnchor="end"
177
+ fontFamily="var(--font-geist-mono)" fontSize="11" letterSpacing="0.79" fill="#a8a8a8">
178
+ {round1(ghostTotal)} AT FP16{ghostClipped ? " ›" : ""}
179
+ </text>
180
+ )}
181
+
182
+ {/* weights that fit — rounded on the left only */}
183
+ <path
184
+ d={`M${R} ${barTop} H${kvStart} V${barBottom} H${R} A${R} ${R} 0 0 1 0 ${barBottom - R} V${barTop + R} A${R} ${R} 0 0 1 ${R} ${barTop} Z`}
185
+ fill="#171717"
186
+ />
187
+ {/* the fixed reservation */}
188
+ <rect x={kvStart} y={barTop} width={Math.max(0, kvEnd - kvStart)} height={BAR_H} fill="#8f8f8f" />
189
+ {/* what will not load */}
190
+ {overflows && (
191
+ <rect x={x(vram)} y={barTop} width={barEnd - x(vram)} height={BAR_H} fill="#ebebeb" />
192
+ )}
193
+
194
+ {/* drop lines continued through the bar */}
195
+ {ticks.map((t) => {
196
+ const accent = PRECISION_ACCENT[t.bits];
197
+ const isSel = t.bits === selectedBits;
198
+ return (
199
+ <line key={t.bits} x1={t.x} y1={barTop + 1} x2={t.x} y2={barBottom - 1} stroke={accent}
200
+ strokeWidth={isSel ? 2.5 : 1.75} strokeLinecap="round"
201
+ strokeDasharray={isSel ? "0.01 4" : "0.01 3.5"}
202
+ opacity={isSel ? 1 : 0.9} />
203
+ );
204
+ })}
205
+
206
+ {/* in-bar figures */}
207
+ {kvStart >= 52 && (
208
+ <text x="16" y={barTop + 32} fontFamily="var(--font-geist-mono)" fontSize="13" fill="#fafafa">
209
+ {round1(overflows ? weightsThatFit : selWeights)}
210
+ </text>
211
+ )}
212
+ {overflows && barEnd - x(vram) >= 52 && (
213
+ <text x={(x(vram) + barEnd) / 2} y={barTop + 32} textAnchor="middle"
214
+ fontFamily="var(--font-geist-mono)" fontSize="13" fill="#171717">
215
+ {round1(selTotal - vram)}
216
+ </text>
217
+ )}
218
+
219
+ {/* capacity line */}
220
+ <line x1={x(vram)} y1={barBottom} x2={x(vram)} y2={barBottom + 12} stroke="#171717" strokeWidth="1.5" />
221
+ <text x={x(vram)} y={barBottom + 30} textAnchor="middle" fontFamily="var(--font-geist-mono)"
222
+ fontSize="13" fill="#171717">
223
+ {vram}
224
+ </text>
225
+
226
+ </svg>
227
+ );
228
+ }
components/ModelInput.tsx ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { useRef, useState } from "react";
4
+ import type { Model } from "@/lib/data";
5
+
6
+ interface Props {
7
+ onResolve: (model: Model | null) => void;
8
+ }
9
+
10
+ type Status =
11
+ | { kind: "empty" }
12
+ | { kind: "loading" }
13
+ | { kind: "error"; message: string }
14
+ | { kind: "ok" };
15
+
16
+ /**
17
+ * A free-text field for a Hub repo id. There are millions of models on the
18
+ * Hub, so there is no list to pick from — the id is typed, and the sizes come
19
+ * back from /api/model.
20
+ */
21
+ export default function ModelInput({ onResolve }: Props) {
22
+ const [text, setText] = useState("");
23
+ const [status, setStatus] = useState<Status>({ kind: "empty" });
24
+ const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
25
+ // Only the newest lookup may write state; earlier ones lose the race.
26
+ const latest = useRef(0);
27
+
28
+ async function lookup(id: string, ticket: number) {
29
+ try {
30
+ const res = await fetch(`/api/model?id=${encodeURIComponent(id)}`);
31
+ const body = await res.json();
32
+ if (ticket !== latest.current) return;
33
+ if (!res.ok) {
34
+ setStatus({ kind: "error", message: body.error ?? "Lookup failed." });
35
+ onResolve(null);
36
+ return;
37
+ }
38
+ setStatus({ kind: "ok" });
39
+ onResolve(body as Model);
40
+ } catch {
41
+ if (ticket !== latest.current) return;
42
+ setStatus({ kind: "error", message: "Could not reach the Hub." });
43
+ onResolve(null);
44
+ }
45
+ }
46
+
47
+ function handleChange(next: string) {
48
+ setText(next);
49
+ clearTimeout(timer.current);
50
+ const id = next.trim();
51
+ const ticket = ++latest.current;
52
+
53
+ if (!id) {
54
+ setStatus({ kind: "empty" });
55
+ onResolve(null);
56
+ return;
57
+ }
58
+
59
+ setStatus({ kind: "loading" });
60
+ timer.current = setTimeout(() => lookup(id, ticket), 400);
61
+ }
62
+
63
+ return (
64
+ <span className="relative inline-flex flex-col">
65
+ <input
66
+ aria-label="Model"
67
+ value={text}
68
+ onChange={(e) => handleChange(e.target.value)}
69
+ placeholder="Qwen/Qwen3-32B"
70
+ spellCheck={false}
71
+ autoCapitalize="off"
72
+ autoComplete="off"
73
+ className="h-[52px] w-[420px] rounded-md bg-paper px-3.5 text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-obsidian ring-raised outline-none placeholder:text-ash"
74
+ />
75
+ <span
76
+ className="label absolute top-[56px] left-1 whitespace-nowrap text-graphite"
77
+ role={status.kind === "error" ? "alert" : undefined}
78
+ >
79
+ {status.kind === "loading" && "Looking up"}
80
+ {status.kind === "error" && status.message}
81
+ </span>
82
+ </span>
83
+ );
84
+ }
components/SentenceCombobox.tsx ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import {
4
+ Combobox,
5
+ ComboboxContent,
6
+ ComboboxEmpty,
7
+ ComboboxInput,
8
+ ComboboxItem,
9
+ ComboboxList,
10
+ ComboboxTrigger,
11
+ ComboboxValue,
12
+ } from "@/components/ui/combobox";
13
+
14
+ export interface Option {
15
+ id: string;
16
+ label: string;
17
+ }
18
+
19
+ interface Props {
20
+ value: string;
21
+ options: Option[];
22
+ onChange: (id: string) => void;
23
+ ariaLabel: string;
24
+ /** shown in the search field inside the popup */
25
+ placeholder?: string;
26
+ /** render the value in placeholder grey — a default nobody has confirmed yet */
27
+ muted?: boolean;
28
+ }
29
+
30
+ /**
31
+ * A type-ahead that reads as part of the sentence. Same chip as
32
+ * SentenceSelect, but the list is long enough (316 hardware configurations)
33
+ * that scrolling it is hopeless — so the popup opens on a search field.
34
+ */
35
+ export default function SentenceCombobox({
36
+ value,
37
+ options,
38
+ onChange,
39
+ ariaLabel,
40
+ placeholder = "Search",
41
+ muted = false,
42
+ }: Props) {
43
+ const current = options.find((o) => o.id === value) ?? options[0];
44
+
45
+ return (
46
+ <Combobox
47
+ items={options}
48
+ value={current}
49
+ onValueChange={(o: Option | null) => o && onChange(o.id)}
50
+ itemToStringLabel={(o: Option) => o.label}
51
+ >
52
+ <ComboboxTrigger
53
+ aria-label={ariaLabel}
54
+ className={`inline-flex h-[52px] items-center gap-2.5 rounded-md bg-paper px-3.5 text-[40px] font-medium leading-[44px] tracking-[-0.04em] ring-raised [&_svg]:text-graphite ${
55
+ muted ? "text-ash" : "text-obsidian"
56
+ }`}
57
+ >
58
+ <ComboboxValue />
59
+ </ComboboxTrigger>
60
+ <ComboboxContent className="min-w-[280px] border-hairline bg-paper">
61
+ <ComboboxInput placeholder={placeholder} showTrigger={false} />
62
+ <ComboboxEmpty className="font-mono text-[13px] text-graphite">
63
+ Nothing matches
64
+ </ComboboxEmpty>
65
+ <ComboboxList>
66
+ {(o: Option) => (
67
+ <ComboboxItem key={o.id} value={o} className="font-mono text-[13px]">
68
+ {o.label}
69
+ </ComboboxItem>
70
+ )}
71
+ </ComboboxList>
72
+ </ComboboxContent>
73
+ </Combobox>
74
+ );
75
+ }
components/SentenceSelect.tsx ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ interface Option {
4
+ id: string;
5
+ label: string;
6
+ }
7
+
8
+ interface Props {
9
+ value: string;
10
+ options: Option[];
11
+ onChange: (id: string) => void;
12
+ /** matches the 40px display type of the surrounding sentence */
13
+ size?: "display" | "inline";
14
+ ariaLabel: string;
15
+ }
16
+
17
+ /**
18
+ * A dropdown that reads as part of a sentence. The native <select> is layered
19
+ * transparently over the rendered text so it stays keyboard and screen-reader
20
+ * navigable while looking like body copy.
21
+ */
22
+ export default function SentenceSelect({
23
+ value,
24
+ options,
25
+ onChange,
26
+ size = "display",
27
+ ariaLabel,
28
+ }: Props) {
29
+ const current = options.find((o) => o.id === value) ?? options[0];
30
+ const display = size === "display";
31
+
32
+ return (
33
+ <span
34
+ className={`relative inline-flex items-center gap-2.5 rounded-md bg-paper ring-raised ${
35
+ display ? "h-[52px] px-3.5" : "h-9 px-3"
36
+ }`}
37
+ >
38
+ <span
39
+ className={
40
+ display
41
+ ? "text-[40px] font-medium leading-[44px] tracking-[-0.04em] text-obsidian"
42
+ : "font-mono text-[13px] text-obsidian"
43
+ }
44
+ >
45
+ {current.label}
46
+ </span>
47
+ <svg
48
+ width="12"
49
+ height="12"
50
+ viewBox="0 0 12 12"
51
+ fill="none"
52
+ aria-hidden
53
+ className="shrink-0"
54
+ >
55
+ <path
56
+ d="M3 4.5L6 7.5L9 4.5"
57
+ stroke="#8f8f8f"
58
+ strokeWidth="1.4"
59
+ strokeLinecap="round"
60
+ strokeLinejoin="round"
61
+ />
62
+ </svg>
63
+ <select
64
+ aria-label={ariaLabel}
65
+ value={value}
66
+ onChange={(e) => onChange(e.target.value)}
67
+ className="absolute inset-0 cursor-pointer opacity-0"
68
+ >
69
+ {options.map((o) => (
70
+ <option key={o.id} value={o.id}>
71
+ {o.label}
72
+ </option>
73
+ ))}
74
+ </select>
75
+ </span>
76
+ );
77
+ }
components/TradeoffMap.tsx ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import { Method } from "@/lib/data";
4
+
5
+ const W = 1120;
6
+ const H = 600;
7
+ const LEFT = 140;
8
+ // The leftmost ink is the rotated y-axis caption, which paints out to x=101.
9
+ // Cropping the viewBox to it lines the map up with the body text.
10
+ const INK_LEFT = 100;
11
+ const RIGHT = 1100;
12
+ const TOP = 40;
13
+ const BOTTOM = 520;
14
+ const MID_X = 620;
15
+ const MID_Y = 280;
16
+ const CHAR_W = 10.2; // Geist Mono at 17px
17
+
18
+ /** Stable pseudo-random in [0,1) from a string, so jitter never moves between renders. */
19
+ function hash01(s: string, salt: number): number {
20
+ let h = 2166136261 ^ salt;
21
+ for (let i = 0; i < s.length; i++) {
22
+ h ^= s.charCodeAt(i);
23
+ h = Math.imul(h, 16777619);
24
+ }
25
+ return ((h >>> 0) % 10000) / 10000;
26
+ }
27
+
28
+ interface Placed {
29
+ method: Method;
30
+ x: number;
31
+ y: number;
32
+ }
33
+
34
+ /**
35
+ * Spread a quadrant's members across evenly-spaced vertical slots, then jitter
36
+ * within each slot. Guarantees no vertical collision while breaking up the
37
+ * column of text that a naive layout produces.
38
+ */
39
+ function place(members: Method[], left: number, right: number, top: number, bottom: number): Placed[] {
40
+ const slot = (bottom - top) / (members.length + 1);
41
+ return members.map((method, i) => {
42
+ const jx = hash01(method.id, 1);
43
+ const jy = hash01(method.id, 2);
44
+ const textW = method.name.length * CHAR_W;
45
+ const usable = Math.max(0, right - left - 56 - textW);
46
+ return {
47
+ method,
48
+ x: left + 28 + jx * usable,
49
+ y: top + slot * (i + 1) + (jy - 0.5) * slot * 0.4,
50
+ };
51
+ });
52
+ }
53
+
54
+ export default function TradeoffMap({ methods }: { methods: Method[] }) {
55
+ const saves = (m: Method) => m.serializable === "yes" || m.serializable === "partial";
56
+
57
+ const topLeft = methods.filter((m) => m.onTheFly && saves(m));
58
+ const topRight = methods.filter((m) => !m.onTheFly && saves(m));
59
+ const bottomLeft = methods.filter((m) => m.onTheFly && !saves(m));
60
+ const bottomRight = methods.filter((m) => !m.onTheFly && !saves(m));
61
+
62
+ const placed = [
63
+ ...place(topLeft, LEFT, MID_X, TOP, MID_Y),
64
+ ...place(topRight, MID_X, RIGHT, TOP, MID_Y),
65
+ ...place(bottomLeft, LEFT, MID_X, MID_Y, BOTTOM),
66
+ ...place(bottomRight, MID_X, RIGHT, MID_Y, BOTTOM),
67
+ ];
68
+
69
+ const emptyQuadrant =
70
+ bottomRight.length === 0
71
+ ? { x: (MID_X + RIGHT) / 2, y: (MID_Y + BOTTOM) / 2 + 20 }
72
+ : bottomLeft.length === 0
73
+ ? { x: (LEFT + MID_X) / 2, y: (MID_Y + BOTTOM) / 2 + 20 }
74
+ : null;
75
+
76
+ return (
77
+ <svg viewBox={`${INK_LEFT} 0 ${W - INK_LEFT} ${H}`} className="h-auto w-full" role="img"
78
+ aria-label="Methods plotted by when they quantize and whether the result can be saved">
79
+ <rect x={LEFT} y={TOP} width={MID_X - LEFT} height={MID_Y - TOP} fill="#f3f7f5" />
80
+ <line x1={LEFT} y1={MID_Y} x2={RIGHT} y2={MID_Y} stroke="#a8a8a8" strokeWidth="1" strokeDasharray="2 5" />
81
+ <line x1={MID_X} y1={TOP} x2={MID_X} y2={BOTTOM} stroke="#a8a8a8" strokeWidth="1" strokeDasharray="2 5" />
82
+
83
+ <text x="112" y="160" textAnchor="middle" transform="rotate(-90 112 160)"
84
+ fontFamily="var(--font-geist-mono)" fontSize="11" letterSpacing="0.79" fill="#171717">
85
+ SAVES TO A CHECKPOINT
86
+ </text>
87
+ <text x="112" y="400" textAnchor="middle" transform="rotate(-90 112 400)"
88
+ fontFamily="var(--font-geist-mono)" fontSize="11" letterSpacing="0.79" fill="#8f8f8f">
89
+ LIVES IN MEMORY ONLY
90
+ </text>
91
+ <text x={LEFT} y="552" fontFamily="var(--font-geist-mono)" fontSize="11" letterSpacing="0.79" fill="#171717">
92
+ QUANTIZES WHEN YOU LOAD
93
+ </text>
94
+ <text x={RIGHT} y="552" textAnchor="end" fontFamily="var(--font-geist-mono)" fontSize="11"
95
+ letterSpacing="0.79" fill="#171717">
96
+ QUANTIZES BEFORE YOU LOAD
97
+ </text>
98
+ <text x={MID_X} y="580" textAnchor="middle" fontFamily="var(--font-geist-mono)" fontSize="10"
99
+ letterSpacing="0.71" fill="#a8a8a8">
100
+ WHEN QUANTIZATION HAPPENS
101
+ </text>
102
+ <text x={LEFT + 12} y={TOP + 22} fontFamily="var(--font-geist-mono)" fontSize="10"
103
+ letterSpacing="0.71" fill="#34785c">
104
+ BEST ZONE
105
+ </text>
106
+
107
+ {placed.map(({ method, x, y }) => (
108
+ <a key={method.id} href={method.docs} target="_blank" rel="noreferrer">
109
+ <text x={x} y={y} fontFamily="var(--font-geist-mono)" fontSize="17" fontWeight="500"
110
+ fill="#171717" className="hover:fill-[#34785c]">
111
+ {method.name}
112
+ </text>
113
+ </a>
114
+ ))}
115
+
116
+ {emptyQuadrant && (
117
+ <text x={emptyQuadrant.x} y={emptyQuadrant.y} textAnchor="middle"
118
+ fontFamily="var(--font-geist-mono)" fontSize="11" letterSpacing="0.79" fill="#c9c9c9">
119
+ NOTHING LANDS HERE
120
+ </text>
121
+ )}
122
+ </svg>
123
+ );
124
+ }
components/ui/button.tsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { Button as ButtonPrimitive } from "@base-ui/react/button"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { cn } from "cn"
4
+
5
+ const buttonVariants = cva(
6
+ "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: "bg-primary text-primary-foreground hover:bg-primary/80",
11
+ outline:
12
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
15
+ ghost:
16
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
17
+ destructive:
18
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
19
+ link: "text-primary underline-offset-4 hover:underline",
20
+ },
21
+ size: {
22
+ default:
23
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
24
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
25
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
26
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
27
+ icon: "size-8",
28
+ "icon-xs":
29
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
30
+ "icon-sm":
31
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
32
+ "icon-lg": "size-9",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ size: "default",
38
+ },
39
+ }
40
+ )
41
+
42
+ function Button({
43
+ className,
44
+ variant = "default",
45
+ size = "default",
46
+ ...props
47
+ }: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
48
+ return (
49
+ <ButtonPrimitive
50
+ data-slot="button"
51
+ className={cn(buttonVariants({ variant, size, className }))}
52
+ {...props}
53
+ />
54
+ )
55
+ }
56
+
57
+ export { Button, buttonVariants }
components/ui/combobox.tsx ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Combobox as ComboboxPrimitive } from "@base-ui/react"
5
+ import { cn } from "cn"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+ import {
9
+ InputGroup,
10
+ InputGroupAddon,
11
+ InputGroupButton,
12
+ InputGroupInput,
13
+ } from "@/components/ui/input-group"
14
+ import { ChevronDownIcon, XIcon, CheckIcon } from "lucide-react"
15
+
16
+ const Combobox = ComboboxPrimitive.Root
17
+
18
+ function ComboboxValue({ ...props }: ComboboxPrimitive.Value.Props) {
19
+ return <ComboboxPrimitive.Value data-slot="combobox-value" {...props} />
20
+ }
21
+
22
+ function ComboboxTrigger({
23
+ className,
24
+ children,
25
+ ...props
26
+ }: ComboboxPrimitive.Trigger.Props) {
27
+ return (
28
+ <ComboboxPrimitive.Trigger
29
+ data-slot="combobox-trigger"
30
+ className={cn("[&_svg:not([class*='size-'])]:size-4", className)}
31
+ {...props}
32
+ >
33
+ {children}
34
+ <ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
35
+ </ComboboxPrimitive.Trigger>
36
+ )
37
+ }
38
+
39
+ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
40
+ return (
41
+ <ComboboxPrimitive.Clear
42
+ data-slot="combobox-clear"
43
+ render={<InputGroupButton variant="ghost" size="icon-xs" />}
44
+ className={cn(className)}
45
+ {...props}
46
+ >
47
+ <XIcon className="pointer-events-none" />
48
+ </ComboboxPrimitive.Clear>
49
+ )
50
+ }
51
+
52
+ function ComboboxInput({
53
+ className,
54
+ children,
55
+ disabled = false,
56
+ showTrigger = true,
57
+ showClear = false,
58
+ ...props
59
+ }: ComboboxPrimitive.Input.Props & {
60
+ showTrigger?: boolean
61
+ showClear?: boolean
62
+ }) {
63
+ return (
64
+ <InputGroup className={cn("w-auto", className)}>
65
+ <ComboboxPrimitive.Input
66
+ render={<InputGroupInput disabled={disabled} />}
67
+ {...props}
68
+ />
69
+ <InputGroupAddon align="inline-end">
70
+ {showTrigger && (
71
+ <InputGroupButton
72
+ size="icon-xs"
73
+ variant="ghost"
74
+ render={<ComboboxTrigger />}
75
+ data-slot="input-group-button"
76
+ className="group-has-data-[slot=combobox-clear]/input-group:hidden data-pressed:bg-transparent"
77
+ disabled={disabled}
78
+ />
79
+ )}
80
+ {showClear && <ComboboxClear disabled={disabled} />}
81
+ </InputGroupAddon>
82
+ {children}
83
+ </InputGroup>
84
+ )
85
+ }
86
+
87
+ function ComboboxContent({
88
+ className,
89
+ side = "bottom",
90
+ sideOffset = 6,
91
+ align = "start",
92
+ alignOffset = 0,
93
+ anchor,
94
+ ...props
95
+ }: ComboboxPrimitive.Popup.Props &
96
+ Pick<
97
+ ComboboxPrimitive.Positioner.Props,
98
+ "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
99
+ >) {
100
+ return (
101
+ <ComboboxPrimitive.Portal>
102
+ <ComboboxPrimitive.Positioner
103
+ side={side}
104
+ sideOffset={sideOffset}
105
+ align={align}
106
+ alignOffset={alignOffset}
107
+ anchor={anchor}
108
+ className="isolate z-50"
109
+ >
110
+ <ComboboxPrimitive.Popup
111
+ data-slot="combobox-content"
112
+ data-chips={!!anchor}
113
+ className={cn("group/combobox-content relative max-h-(--available-height) w-(--anchor-width) max-w-(--available-width) min-w-[calc(var(--anchor-width)+--spacing(7))] origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[chips=true]:min-w-(--anchor-width) data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 *:data-[slot=input-group]:m-1 *:data-[slot=input-group]:mb-0 *:data-[slot=input-group]:h-8 *:data-[slot=input-group]:border-input/30 *:data-[slot=input-group]:bg-input/30 *:data-[slot=input-group]:shadow-none data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
114
+ {...props}
115
+ />
116
+ </ComboboxPrimitive.Positioner>
117
+ </ComboboxPrimitive.Portal>
118
+ )
119
+ }
120
+
121
+ function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
122
+ return (
123
+ <ComboboxPrimitive.List
124
+ data-slot="combobox-list"
125
+ className={cn(
126
+ "no-scrollbar max-h-[min(calc(--spacing(72)---spacing(9)),calc(var(--available-height)---spacing(9)))] scroll-py-1 overflow-y-auto overscroll-contain p-1 data-empty:p-0",
127
+ className
128
+ )}
129
+ {...props}
130
+ />
131
+ )
132
+ }
133
+
134
+ function ComboboxItem({
135
+ className,
136
+ children,
137
+ ...props
138
+ }: ComboboxPrimitive.Item.Props) {
139
+ return (
140
+ <ComboboxPrimitive.Item
141
+ data-slot="combobox-item"
142
+ className={cn(
143
+ "relative flex w-full cursor-default items-center gap-2 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-highlighted:bg-accent data-highlighted:text-accent-foreground not-data-[variant=destructive]:data-highlighted:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
144
+ className
145
+ )}
146
+ {...props}
147
+ >
148
+ {children}
149
+ <ComboboxPrimitive.ItemIndicator
150
+ render={
151
+ <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" />
152
+ }
153
+ >
154
+ <CheckIcon className="pointer-events-none" />
155
+ </ComboboxPrimitive.ItemIndicator>
156
+ </ComboboxPrimitive.Item>
157
+ )
158
+ }
159
+
160
+ function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
161
+ return (
162
+ <ComboboxPrimitive.Group
163
+ data-slot="combobox-group"
164
+ className={cn(className)}
165
+ {...props}
166
+ />
167
+ )
168
+ }
169
+
170
+ function ComboboxLabel({
171
+ className,
172
+ ...props
173
+ }: ComboboxPrimitive.GroupLabel.Props) {
174
+ return (
175
+ <ComboboxPrimitive.GroupLabel
176
+ data-slot="combobox-label"
177
+ className={cn("px-2 py-1.5 text-xs text-muted-foreground", className)}
178
+ {...props}
179
+ />
180
+ )
181
+ }
182
+
183
+ function ComboboxCollection({ ...props }: ComboboxPrimitive.Collection.Props) {
184
+ return (
185
+ <ComboboxPrimitive.Collection data-slot="combobox-collection" {...props} />
186
+ )
187
+ }
188
+
189
+ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
190
+ return (
191
+ <ComboboxPrimitive.Empty
192
+ data-slot="combobox-empty"
193
+ className={cn(
194
+ "hidden w-full justify-center py-2 text-center text-sm text-muted-foreground group-data-empty/combobox-content:flex",
195
+ className
196
+ )}
197
+ {...props}
198
+ />
199
+ )
200
+ }
201
+
202
+ function ComboboxSeparator({
203
+ className,
204
+ ...props
205
+ }: ComboboxPrimitive.Separator.Props) {
206
+ return (
207
+ <ComboboxPrimitive.Separator
208
+ data-slot="combobox-separator"
209
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
210
+ {...props}
211
+ />
212
+ )
213
+ }
214
+
215
+ function ComboboxChips({
216
+ className,
217
+ ...props
218
+ }: React.ComponentPropsWithRef<typeof ComboboxPrimitive.Chips> &
219
+ ComboboxPrimitive.Chips.Props) {
220
+ return (
221
+ <ComboboxPrimitive.Chips
222
+ data-slot="combobox-chips"
223
+ className={cn(
224
+ "flex min-h-8 flex-wrap items-center gap-1 rounded-lg border border-input bg-transparent bg-clip-padding px-2.5 py-1 text-sm transition-colors focus-within:border-ring focus-within:ring-3 focus-within:ring-ring/50 has-aria-invalid:border-destructive has-aria-invalid:ring-3 has-aria-invalid:ring-destructive/20 has-data-[slot=combobox-chip]:px-1 dark:bg-input/30 dark:has-aria-invalid:border-destructive/50 dark:has-aria-invalid:ring-destructive/40",
225
+ className
226
+ )}
227
+ {...props}
228
+ />
229
+ )
230
+ }
231
+
232
+ function ComboboxChip({
233
+ className,
234
+ children,
235
+ showRemove = true,
236
+ ...props
237
+ }: ComboboxPrimitive.Chip.Props & {
238
+ showRemove?: boolean
239
+ }) {
240
+ return (
241
+ <ComboboxPrimitive.Chip
242
+ data-slot="combobox-chip"
243
+ className={cn(
244
+ "flex h-[calc(--spacing(5.25))] w-fit items-center justify-center gap-1 rounded-sm bg-muted px-1.5 text-xs font-medium whitespace-nowrap text-foreground has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50 has-data-[slot=combobox-chip-remove]:pr-0",
245
+ className
246
+ )}
247
+ {...props}
248
+ >
249
+ {children}
250
+ {showRemove && (
251
+ <ComboboxPrimitive.ChipRemove
252
+ render={<Button variant="ghost" size="icon-xs" />}
253
+ className="-ml-1 opacity-50 hover:opacity-100"
254
+ data-slot="combobox-chip-remove"
255
+ >
256
+ <XIcon className="pointer-events-none" />
257
+ </ComboboxPrimitive.ChipRemove>
258
+ )}
259
+ </ComboboxPrimitive.Chip>
260
+ )
261
+ }
262
+
263
+ function ComboboxChipsInput({
264
+ className,
265
+ ...props
266
+ }: ComboboxPrimitive.Input.Props) {
267
+ return (
268
+ <ComboboxPrimitive.Input
269
+ data-slot="combobox-chip-input"
270
+ className={cn("min-w-16 flex-1 outline-none", className)}
271
+ {...props}
272
+ />
273
+ )
274
+ }
275
+
276
+ function useComboboxAnchor() {
277
+ return React.useRef<HTMLDivElement | null>(null)
278
+ }
279
+
280
+ export {
281
+ Combobox,
282
+ ComboboxInput,
283
+ ComboboxContent,
284
+ ComboboxList,
285
+ ComboboxItem,
286
+ ComboboxGroup,
287
+ ComboboxLabel,
288
+ ComboboxCollection,
289
+ ComboboxEmpty,
290
+ ComboboxSeparator,
291
+ ComboboxChips,
292
+ ComboboxChip,
293
+ ComboboxChipsInput,
294
+ ComboboxTrigger,
295
+ ComboboxValue,
296
+ useComboboxAnchor,
297
+ }
components/ui/input-group.tsx ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cva, type VariantProps } from "class-variance-authority"
5
+ import { cn } from "cn"
6
+
7
+ import { Button } from "@/components/ui/button"
8
+ import { Input } from "@/components/ui/input"
9
+ import { Textarea } from "@/components/ui/textarea"
10
+
11
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
+ return (
13
+ <div
14
+ data-slot="input-group"
15
+ role="group"
16
+ className={cn(
17
+ "group/input-group relative flex h-8 w-full min-w-0 items-center rounded-lg border border-input transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-3 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/50 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-3 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-disabled:bg-input/80 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
18
+ className
19
+ )}
20
+ {...props}
21
+ />
22
+ )
23
+ }
24
+
25
+ const inputGroupAddonVariants = cva(
26
+ "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
27
+ {
28
+ variants: {
29
+ align: {
30
+ "inline-start":
31
+ "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
32
+ "inline-end":
33
+ "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
34
+ "block-start":
35
+ "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
36
+ "block-end":
37
+ "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
38
+ },
39
+ },
40
+ defaultVariants: {
41
+ align: "inline-start",
42
+ },
43
+ }
44
+ )
45
+
46
+ function InputGroupAddon({
47
+ className,
48
+ align = "inline-start",
49
+ ...props
50
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
51
+ return (
52
+ <div
53
+ role="group"
54
+ data-slot="input-group-addon"
55
+ data-align={align}
56
+ className={cn(inputGroupAddonVariants({ align }), className)}
57
+ onClick={(e) => {
58
+ if ((e.target as HTMLElement).closest("button")) {
59
+ return
60
+ }
61
+ e.currentTarget.parentElement?.querySelector("input")?.focus()
62
+ }}
63
+ {...props}
64
+ />
65
+ )
66
+ }
67
+
68
+ const inputGroupButtonVariants = cva(
69
+ "flex items-center gap-2 text-sm shadow-none",
70
+ {
71
+ variants: {
72
+ size: {
73
+ xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
74
+ sm: "",
75
+ "icon-xs":
76
+ "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
77
+ "icon-sm": "size-8 p-0 has-[>svg]:p-0",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ size: "xs",
82
+ },
83
+ }
84
+ )
85
+
86
+ function InputGroupButton({
87
+ className,
88
+ type = "button",
89
+ variant = "ghost",
90
+ size = "xs",
91
+ ...props
92
+ }: Omit<React.ComponentProps<typeof Button>, "size" | "type"> &
93
+ VariantProps<typeof inputGroupButtonVariants> & {
94
+ type?: "button" | "submit" | "reset"
95
+ }) {
96
+ return (
97
+ <Button
98
+ type={type}
99
+ data-size={size}
100
+ variant={variant}
101
+ className={cn(inputGroupButtonVariants({ size }), className)}
102
+ {...props}
103
+ />
104
+ )
105
+ }
106
+
107
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
108
+ return (
109
+ <span
110
+ className={cn(
111
+ "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
112
+ className
113
+ )}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function InputGroupInput({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<"input">) {
123
+ return (
124
+ <Input
125
+ data-slot="input-group-control"
126
+ className={cn(
127
+ "flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
128
+ className
129
+ )}
130
+ {...props}
131
+ />
132
+ )
133
+ }
134
+
135
+ function InputGroupTextarea({
136
+ className,
137
+ ...props
138
+ }: React.ComponentProps<"textarea">) {
139
+ return (
140
+ <Textarea
141
+ data-slot="input-group-control"
142
+ className={cn(
143
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 disabled:bg-transparent aria-invalid:ring-0 dark:bg-transparent dark:disabled:bg-transparent",
144
+ className
145
+ )}
146
+ {...props}
147
+ />
148
+ )
149
+ }
150
+
151
+ export {
152
+ InputGroup,
153
+ InputGroupAddon,
154
+ InputGroupButton,
155
+ InputGroupText,
156
+ InputGroupInput,
157
+ InputGroupTextarea,
158
+ }
components/ui/input.tsx ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { Input as InputPrimitive } from "@base-ui/react/input"
3
+ import { cn } from "cn"
4
+
5
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
6
+ return (
7
+ <InputPrimitive
8
+ type={type}
9
+ data-slot="input"
10
+ className={cn(
11
+ "h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
12
+ className
13
+ )}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ export { Input }
components/ui/textarea.tsx ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import * as React from "react"
2
+ import { cn } from "cn"
3
+
4
+ function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
5
+ return (
6
+ <textarea
7
+ data-slot="textarea"
8
+ className={cn(
9
+ "flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
10
+ className
11
+ )}
12
+ {...props}
13
+ />
14
+ )
15
+ }
16
+
17
+ export { Textarea }
lib/data.ts ADDED
@@ -0,0 +1,252 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { HARDWARE, hardwareLabel } from "./hardware";
2
+
3
+ export type Support = "yes" | "no" | "partial" | "unknown";
4
+
5
+ export type Backend = "cuda" | "rocm" | "metal" | "intel" | "cpu";
6
+
7
+ export interface Model {
8
+ id: string;
9
+ label: string;
10
+ /** billions of parameters */
11
+ params: number;
12
+ layers: number;
13
+ kvHeads: number;
14
+ headDim: number;
15
+ }
16
+
17
+ export interface Gpu {
18
+ id: string;
19
+ label: string;
20
+ /** GB of VRAM */
21
+ vram: number;
22
+ backend: Backend;
23
+ }
24
+
25
+ export interface Goal {
26
+ id: string;
27
+ label: string;
28
+ /** capability that must be supported for this goal */
29
+ requires?: "peft" | "serializable";
30
+ }
31
+
32
+ export interface Method {
33
+ id: string;
34
+ name: string;
35
+ bits: number[];
36
+ backends: Backend[];
37
+ onTheFly: boolean;
38
+ compile: Support;
39
+ peft: Support;
40
+ serializable: Support;
41
+ /** minutes of setup before you can load */
42
+ setupMinutes: number;
43
+ blurb: string;
44
+ config: string;
45
+ docs: string;
46
+ }
47
+
48
+
49
+ /**
50
+ * Every memory configuration in the Hugging Face hardware table, most-owned
51
+ * first — so the cards people actually have sit at the top of the list.
52
+ */
53
+ export const GPUS: Gpu[] = HARDWARE.map((h) => ({
54
+ id: h.id,
55
+ label: hardwareLabel(h),
56
+ vram: h.vram,
57
+ backend: h.backend,
58
+ }));
59
+
60
+ export const GOALS: Goal[] = [
61
+ { id: "serve", label: "serve requests" },
62
+ { id: "finetune", label: "fine-tune it", requires: "peft" },
63
+ { id: "publish", label: "publish a checkpoint", requires: "serializable" },
64
+ ];
65
+
66
+ export const PRECISIONS = [8, 4, 3, 2] as const;
67
+
68
+ export const PRECISION_ACCENT: Record<number, string> = {
69
+ 8: "#c08532",
70
+ 4: "#34785c",
71
+ 3: "#f54e00",
72
+ 2: "#cf2d56",
73
+ };
74
+
75
+ export const METHODS: Method[] = [
76
+ {
77
+ id: "bitsandbytes",
78
+ name: "bitsandbytes",
79
+ bits: [4, 8],
80
+ backends: ["cuda", "rocm", "metal", "intel", "cpu"],
81
+ onTheFly: true,
82
+ compile: "yes",
83
+ peft: "yes",
84
+ serializable: "yes",
85
+ setupMinutes: 0,
86
+ blurb: "Quantizes as the model loads. No calibration data, no extra pipeline.",
87
+ config: "BitsAndBytesConfig(load_in_4bit=True)",
88
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/bitsandbytes",
89
+ },
90
+ {
91
+ id: "gptqmodel",
92
+ name: "GPTQModel",
93
+ bits: [2, 3, 4, 8],
94
+ backends: ["cuda", "rocm", "metal", "intel", "cpu"],
95
+ onTheFly: false,
96
+ compile: "no",
97
+ peft: "yes",
98
+ serializable: "yes",
99
+ setupMinutes: 25,
100
+ blurb: "Best accuracy available at 4-bit. Costs a calibration pass up front.",
101
+ config: 'GPTQConfig(bits=4, dataset="c4")',
102
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/gptq",
103
+ },
104
+ {
105
+ id: "awq",
106
+ name: "AWQ",
107
+ bits: [4],
108
+ backends: ["cuda", "rocm", "intel", "cpu"],
109
+ onTheFly: false,
110
+ compile: "unknown",
111
+ peft: "yes",
112
+ serializable: "yes",
113
+ setupMinutes: 20,
114
+ blurb: "Activation-aware and widely deployed. Slightly behind GPTQ on accuracy.",
115
+ config: "AwqConfig(bits=4)",
116
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/awq",
117
+ },
118
+ {
119
+ id: "hqq",
120
+ name: "HQQ",
121
+ bits: [2, 3, 4, 8],
122
+ backends: ["cuda", "intel", "cpu"],
123
+ onTheFly: true,
124
+ compile: "yes",
125
+ peft: "yes",
126
+ serializable: "no",
127
+ setupMinutes: 0,
128
+ blurb: "Fast to apply and needs no dataset, but the result cannot be saved.",
129
+ config: "HqqConfig(nbits=4, group_size=64)",
130
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/hqq",
131
+ },
132
+ {
133
+ id: "compressed-tensors",
134
+ name: "compressed-tensors",
135
+ bits: [4, 8],
136
+ backends: ["cuda", "rocm", "intel", "cpu"],
137
+ onTheFly: false,
138
+ compile: "no",
139
+ peft: "yes",
140
+ serializable: "yes",
141
+ setupMinutes: 0,
142
+ blurb: "Loads checkpoints that are already quantized, and sparse as well.",
143
+ config: "load a compressed-tensors checkpoint",
144
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/compressed_tensors",
145
+ },
146
+ {
147
+ id: "torchao",
148
+ name: "torchao",
149
+ bits: [4, 8],
150
+ backends: ["cuda", "metal", "intel", "cpu"],
151
+ onTheFly: true,
152
+ compile: "unknown",
153
+ peft: "unknown",
154
+ serializable: "partial",
155
+ setupMinutes: 0,
156
+ blurb: "Native to PyTorch. Three of the four capabilities are unverified.",
157
+ config: 'TorchAoConfig("int4_weight_only")',
158
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/torchao",
159
+ },
160
+ {
161
+ id: "optimum-quanto",
162
+ name: "optimum-quanto",
163
+ bits: [2, 4, 8],
164
+ backends: ["cuda", "metal", "intel", "cpu"],
165
+ onTheFly: true,
166
+ compile: "yes",
167
+ peft: "no",
168
+ serializable: "no",
169
+ setupMinutes: 0,
170
+ blurb: "Portable across backends, including Metal. Cannot save the result.",
171
+ config: "QuantoConfig(weights='int4')",
172
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/quanto",
173
+ },
174
+ {
175
+ id: "higgs",
176
+ name: "HIGGS",
177
+ bits: [2, 4],
178
+ backends: ["cuda"],
179
+ onTheFly: true,
180
+ compile: "yes",
181
+ peft: "no",
182
+ serializable: "yes",
183
+ setupMinutes: 0,
184
+ blurb: "Calibration-free at 2 and 4 bits. No PEFT support.",
185
+ config: "HiggsConfig(bits=4)",
186
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/higgs",
187
+ },
188
+ {
189
+ id: "aqlm",
190
+ name: "AQLM",
191
+ bits: [2],
192
+ backends: ["cuda", "intel", "cpu"],
193
+ onTheFly: false,
194
+ compile: "yes",
195
+ peft: "yes",
196
+ serializable: "yes",
197
+ setupMinutes: 90,
198
+ blurb: "Additive quantization for extreme compression. Long calibration.",
199
+ config: "load an AQLM checkpoint",
200
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/aqlm",
201
+ },
202
+ {
203
+ id: "finegrained-fp8",
204
+ name: "FINEGRAINED_FP8",
205
+ bits: [8],
206
+ backends: ["cuda", "intel"],
207
+ onTheFly: true,
208
+ compile: "no",
209
+ peft: "no",
210
+ serializable: "yes",
211
+ setupMinutes: 0,
212
+ blurb: "Built into Transformers, so there is no extra dependency to pin.",
213
+ config: "FineGrainedFP8Config()",
214
+ docs: "https://huggingface.co/docs/transformers/main/en/quantization/finegrained_fp8",
215
+ },
216
+ ];
217
+
218
+ /** Weight bytes, in decimal GB. */
219
+ export function weightsGB(model: Model, bits: number): number {
220
+ return (model.params * bits) / 8;
221
+ }
222
+
223
+ /** KV cache for a given context length, in decimal GB (fp16 K and V). */
224
+ export function kvCacheGB(model: Model, contextTokens: number): number {
225
+ const bytesPerToken = 2 * model.layers * model.kvHeads * model.headDim * 2;
226
+ return (bytesPerToken * contextTokens) / 1e9;
227
+ }
228
+
229
+ /** Rough allowance for activations and CUDA workspace, in decimal GB. */
230
+ export const ACTIVATIONS_GB = 1.5;
231
+
232
+ export function overheadGB(model: Model, contextTokens: number): number {
233
+ return kvCacheGB(model, contextTokens) + ACTIVATIONS_GB;
234
+ }
235
+
236
+ export function totalGB(model: Model, bits: number, contextTokens: number): number {
237
+ return weightsGB(model, bits) + overheadGB(model, contextTokens);
238
+ }
239
+
240
+ export function methodsFor(bits: number, backend: Backend, goal: Goal): Method[] {
241
+ return METHODS.filter((m) => {
242
+ if (!m.bits.includes(bits)) return false;
243
+ if (!m.backends.includes(backend)) return false;
244
+ if (goal.requires === "peft" && m.peft !== "yes") return false;
245
+ if (goal.requires === "serializable" && m.serializable === "no") return false;
246
+ return true;
247
+ });
248
+ }
249
+
250
+ export function round1(n: number): number {
251
+ return Math.round(n * 10) / 10;
252
+ }
lib/hardware.ts ADDED
@@ -0,0 +1,350 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Backend } from "./data";
2
+
3
+ /**
4
+ * The hardware catalog from https://huggingface.co/hardware?view=table — the
5
+ * Hardware and Memory columns only, in the table's own order (most owners
6
+ * first). CPUs are omitted: the table lists no memory for them, because they
7
+ * run out of system RAM rather than a fixed pool.
8
+ *
9
+ * One entry per memory configuration, not per device: an RTX 3060 is sold as
10
+ * both 8GB and 12GB, and which one you own decides what fits. `id` is the
11
+ * page's own slug for that configuration.
12
+ *
13
+ * Every field here is scraped except `backend` — the table has no such column,
14
+ * so it is derived from the vendor. See the note in the README.
15
+ */
16
+ export interface HardwareConfig {
17
+ /** the page's slug, e.g. "a100-80gb" */
18
+ id: string;
19
+ name: string;
20
+ vendor: "NVIDIA" | "AMD" | "Apple" | "INTEL";
21
+ /** the product line, as the table labels it (e.g. "NVIDIA SXM") */
22
+ family: string;
23
+ backend: Backend;
24
+ /** GB of memory in this configuration */
25
+ vram: number;
26
+ }
27
+
28
+ export const HARDWARE: HardwareConfig[] = [
29
+ { id: "rtx-3060-8gb", name: "RTX 3060", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
30
+ { id: "rtx-3060-12gb", name: "RTX 3060", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
31
+ { id: "rtx-3090", name: "RTX 3090", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 24 },
32
+ { id: "apple-m4-16gb", name: "Apple M4", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 16 },
33
+ { id: "apple-m4-24gb", name: "Apple M4", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 24 },
34
+ { id: "apple-m4-32gb", name: "Apple M4", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 32 },
35
+ { id: "rtx-5090", name: "RTX 5090", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 32 },
36
+ { id: "rtx-4090", name: "RTX 4090", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 24 },
37
+ { id: "apple-m4-pro-24gb", name: "Apple M4 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 24 },
38
+ { id: "apple-m4-pro-48gb", name: "Apple M4 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 48 },
39
+ { id: "apple-m4-pro-64gb", name: "Apple M4 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 64 },
40
+ { id: "rtx-5070-ti", name: "RTX 5070 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
41
+ { id: "rtx-5060-ti-8gb", name: "RTX 5060 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
42
+ { id: "rtx-5060-ti-16gb", name: "RTX 5060 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
43
+ { id: "apple-m1-pro-16gb", name: "Apple M1 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 16 },
44
+ { id: "apple-m1-pro-24gb", name: "Apple M1 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 24 },
45
+ { id: "apple-m1-pro-32gb", name: "Apple M1 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 32 },
46
+ { id: "apple-m1-8gb", name: "Apple M1", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 8 },
47
+ { id: "apple-m1-16gb", name: "Apple M1", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 16 },
48
+ { id: "apple-m4-max-36gb", name: "Apple M4 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 36 },
49
+ { id: "apple-m4-max-48gb", name: "Apple M4 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 48 },
50
+ { id: "apple-m4-max-64gb", name: "Apple M4 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 64 },
51
+ { id: "apple-m4-max-128gb", name: "Apple M4 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 128 },
52
+ { id: "rtx-5080", name: "RTX 5080", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
53
+ { id: "gb10", name: "GB10", vendor: "NVIDIA", family: "DGX Spark", backend: "cuda", vram: 128 },
54
+ { id: "rtx-3080-10gb", name: "RTX 3080", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 10 },
55
+ { id: "rtx-3080-12gb", name: "RTX 3080", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
56
+ { id: "rtx-4060", name: "RTX 4060", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
57
+ { id: "apple-m1-max-16gb", name: "Apple M1 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 16 },
58
+ { id: "apple-m1-max-24gb", name: "Apple M1 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 24 },
59
+ { id: "apple-m1-max-32gb", name: "Apple M1 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 32 },
60
+ { id: "apple-m1-max-64gb", name: "Apple M1 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 64 },
61
+ { id: "rtx-3070", name: "RTX 3070", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
62
+ { id: "rtx-4060-ti-8gb", name: "RTX 4060 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
63
+ { id: "rtx-4060-ti-16gb", name: "RTX 4060 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
64
+ { id: "rx-9070-xt", name: "RX 9070 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
65
+ { id: "rtx-4070", name: "RTX 4070", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
66
+ { id: "rtx-3050-mobile-4gb", name: "RTX 3050 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 4 },
67
+ { id: "rtx-3050-mobile-6gb", name: "RTX 3050 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 6 },
68
+ { id: "apple-m5-pro-24gb", name: "Apple M5 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 24 },
69
+ { id: "apple-m5-pro-36gb", name: "Apple M5 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 36 },
70
+ { id: "apple-m5-pro-48gb", name: "Apple M5 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 48 },
71
+ { id: "apple-m5-pro-64gb", name: "Apple M5 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 64 },
72
+ { id: "apple-m3-pro-18gb", name: "Apple M3 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 18 },
73
+ { id: "apple-m3-pro-36gb", name: "Apple M3 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 36 },
74
+ { id: "apple-m5-max-36gb", name: "Apple M5 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 36 },
75
+ { id: "apple-m5-max-48gb", name: "Apple M5 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 48 },
76
+ { id: "apple-m5-max-64gb", name: "Apple M5 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 64 },
77
+ { id: "apple-m5-max-128gb", name: "Apple M5 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 128 },
78
+ { id: "rtx-4060-laptop", name: "RTX 4060 Laptop", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
79
+ { id: "apple-m2-8gb", name: "Apple M2", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 8 },
80
+ { id: "apple-m2-16gb", name: "Apple M2", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 16 },
81
+ { id: "apple-m2-24gb", name: "Apple M2", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 24 },
82
+ { id: "rtx-5070", name: "RTX 5070", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
83
+ { id: "apple-m5-16gb", name: "Apple M5", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 16 },
84
+ { id: "apple-m5-24gb", name: "Apple M5", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 24 },
85
+ { id: "apple-m5-32gb", name: "Apple M5", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 32 },
86
+ { id: "rtx-3060-ti", name: "RTX 3060 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
87
+ { id: "rtx-4070-ti-super", name: "RTX 4070 Ti Super", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
88
+ { id: "ryzen-ai-max-395-64gb", name: "Ryzen AI Max+ 395", vendor: "AMD", family: "AMD Ryzen AI Max", backend: "rocm", vram: 64 },
89
+ { id: "ryzen-ai-max-395-96gb", name: "Ryzen AI Max+ 395", vendor: "AMD", family: "AMD Ryzen AI Max", backend: "rocm", vram: 96 },
90
+ { id: "ryzen-ai-max-395-128gb", name: "Ryzen AI Max+ 395", vendor: "AMD", family: "AMD Ryzen AI Max", backend: "rocm", vram: 128 },
91
+ { id: "apple-m3-max-36gb", name: "Apple M3 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 36 },
92
+ { id: "apple-m3-max-48gb", name: "Apple M3 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 48 },
93
+ { id: "apple-m3-max-64gb", name: "Apple M3 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 64 },
94
+ { id: "apple-m3-max-96gb", name: "Apple M3 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 96 },
95
+ { id: "apple-m3-max-128gb", name: "Apple M3 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 128 },
96
+ { id: "rx-7900-xtx", name: "RX 7900 XTX", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 24 },
97
+ { id: "gtx-1660", name: "GTX 1660", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
98
+ { id: "gtx-1650-mobile", name: "GTX 1650 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
99
+ { id: "rtx-4070-super", name: "RTX 4070 Super", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
100
+ { id: "apple-m2-pro-16gb", name: "Apple M2 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 16 },
101
+ { id: "apple-m2-pro-24gb", name: "Apple M2 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 24 },
102
+ { id: "apple-m2-pro-32gb", name: "Apple M2 Pro", vendor: "Apple", family: "Apple Silicon Pro", backend: "metal", vram: 32 },
103
+ { id: "rtx-4080", name: "RTX 4080", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
104
+ { id: "rtx-4070-ti", name: "RTX 4070 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
105
+ { id: "rtx-4080-super", name: "RTX 4080 SUPER", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
106
+ { id: "rtx-3080-ti", name: "RTX 3080 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
107
+ { id: "h100", name: "H100", vendor: "NVIDIA", family: "NVIDIA SXM", backend: "cuda", vram: 80 },
108
+ { id: "apple-m3-8gb", name: "Apple M3", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 8 },
109
+ { id: "apple-m3-16gb", name: "Apple M3", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 16 },
110
+ { id: "apple-m3-24gb", name: "Apple M3", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 24 },
111
+ { id: "rtx-pro-6000-ws", name: "RTX PRO 6000 WS", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 96 },
112
+ { id: "a100-40gb", name: "A100", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 40 },
113
+ { id: "a100-80gb", name: "A100", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 80 },
114
+ { id: "apple-m2-max-32gb", name: "Apple M2 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 32 },
115
+ { id: "apple-m2-max-64gb", name: "Apple M2 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 64 },
116
+ { id: "apple-m2-max-96gb", name: "Apple M2 Max", vendor: "Apple", family: "Apple Silicon Max", backend: "metal", vram: 96 },
117
+ { id: "t4", name: "T4", vendor: "NVIDIA", family: "NVIDIA T4", backend: "cuda", vram: 16 },
118
+ { id: "rtx-5060", name: "RTX 5060", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
119
+ { id: "apple-m3-ultra-96gb", name: "Apple M3 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 96 },
120
+ { id: "apple-m3-ultra-256gb", name: "Apple M3 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 256 },
121
+ { id: "apple-m3-ultra-512gb", name: "Apple M3 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 512 },
122
+ { id: "gtx-1060-3gb", name: "GTX 1060", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 3 },
123
+ { id: "gtx-1060-6gb", name: "GTX 1060", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
124
+ { id: "rtx-4050-laptop", name: "RTX 4050 Laptop", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 6 },
125
+ { id: "rtx-3070-ti", name: "RTX 3070 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
126
+ { id: "gtx-1070-ti", name: "GTX 1070 Ti", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
127
+ { id: "rx-6700-xt", name: "RX 6700 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 12 },
128
+ { id: "gtx-1050-ti", name: "GTX 1050 Ti", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
129
+ { id: "rtx-2080-ti-11gb", name: "RTX 2080 Ti", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 11 },
130
+ { id: "rtx-2080-ti-22gb", name: "RTX 2080 Ti", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 22 },
131
+ { id: "rx-7800-xt", name: "RX 7800 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
132
+ { id: "gtx-1080-ti", name: "GTX 1080 Ti", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 11 },
133
+ { id: "v100-16gb", name: "V100", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
134
+ { id: "v100-32gb", name: "V100", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 32 },
135
+ { id: "rtx-3060-mobile", name: "RTX 3060 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 6 },
136
+ { id: "rtx-2070-super", name: "RTX 2070 SUPER", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
137
+ { id: "rtx-3090-ti", name: "RTX 3090 Ti", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 24 },
138
+ { id: "rtx-2060", name: "RTX 2060", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
139
+ { id: "rx-6800", name: "RX 6800", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
140
+ { id: "rtx-2070", name: "RTX 2070", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
141
+ { id: "r9700-pro", name: "R9700 PRO", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 32 },
142
+ { id: "rx-7900-xt", name: "RX 7900 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 20 },
143
+ { id: "rx-9060-xt-8gb", name: "RX 9060 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
144
+ { id: "rx-9060-xt-16gb", name: "RX 9060 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
145
+ { id: "h200", name: "H200", vendor: "NVIDIA", family: "NVIDIA SXM", backend: "cuda", vram: 141 },
146
+ { id: "rx-6600", name: "RX 6600", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
147
+ { id: "rtx-pro-6000-max-q", name: "RTX PRO 6000 Max-Q", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 96 },
148
+ { id: "rtx-2080", name: "RTX 2080", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
149
+ { id: "rtx-6000-ada", name: "RTX 6000 Ada", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
150
+ { id: "rtx-4070-mobile", name: "RTX 4070 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
151
+ { id: "rtx-4070-laptop", name: "RTX 4070 Laptop", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
152
+ { id: "rtx-3070-ti-mobile", name: "RTX 3070 Ti Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
153
+ { id: "rx-6950-xt", name: "RX 6950 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
154
+ { id: "rtx-a6000", name: "RTX A6000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
155
+ { id: "rx-7600-xt-8gb", name: "RX 7600 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
156
+ { id: "rx-7600-xt-16gb", name: "RX 7600 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
157
+ { id: "rtx-2060-mobile", name: "RTX 2060 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
158
+ { id: "p40", name: "P40", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
159
+ { id: "rtx-a4000", name: "RTX A4000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
160
+ { id: "l40s", name: "L40s", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
161
+ { id: "rtx-5070-mobile", name: "RTX 5070 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
162
+ { id: "rx-9070", name: "RX 9070", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
163
+ { id: "arc-b580", name: "Arc B580", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 12 },
164
+ { id: "l4", name: "L4", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
165
+ { id: "b200", name: "B200", vendor: "NVIDIA", family: "NVIDIA SXM", backend: "cuda", vram: 192 },
166
+ { id: "arc-a770-8gb", name: "Arc A770", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 8 },
167
+ { id: "arc-a770-16gb", name: "Arc A770", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 16 },
168
+ { id: "apple-m2-ultra-64gb", name: "Apple M2 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 64 },
169
+ { id: "apple-m2-ultra-96gb", name: "Apple M2 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 96 },
170
+ { id: "apple-m2-ultra-128gb", name: "Apple M2 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 128 },
171
+ { id: "apple-m2-ultra-192gb", name: "Apple M2 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 192 },
172
+ { id: "rtx-5060-mobile", name: "RTX 5060 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
173
+ { id: "rtx-5090-mobile", name: "RTX 5090 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 24 },
174
+ { id: "rtx-5080-mobile", name: "RTX 5080 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
175
+ { id: "rtx-5070-ti-mobile", name: "RTX 5070 Ti Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
176
+ { id: "apple-m1-ultra-16gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 16 },
177
+ { id: "apple-m1-ultra-24gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 24 },
178
+ { id: "apple-m1-ultra-32gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 32 },
179
+ { id: "apple-m1-ultra-64gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 64 },
180
+ { id: "apple-m1-ultra-96gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 96 },
181
+ { id: "apple-m1-ultra-128gb", name: "Apple M1 Ultra", vendor: "Apple", family: "Apple Silicon Ultra", backend: "metal", vram: 128 },
182
+ { id: "rtx-a2000-6gb", name: "RTX A2000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
183
+ { id: "rtx-a2000-12gb", name: "RTX A2000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 12 },
184
+ { id: "arc-pro-b70", name: "Arc Pro B70", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 32 },
185
+ { id: "rtx-4000-ada", name: "RTX 4000 Ada", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 20 },
186
+ { id: "rx-6600-xt", name: "RX 6600 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
187
+ { id: "rtx-2000-ada", name: "RTX 2000 Ada", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
188
+ { id: "jetson-orin-nano-8gb", name: "Jetson Orin Nano 8GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 8 },
189
+ { id: "p100", name: "P100", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
190
+ { id: "rx-5500-xt-4gb", name: "RX 5500 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 4 },
191
+ { id: "rx-5500-xt-8gb", name: "RX 5500 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
192
+ { id: "radeon-780m-16gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
193
+ { id: "radeon-780m-24gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
194
+ { id: "radeon-780m-32gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
195
+ { id: "radeon-780m-48gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
196
+ { id: "radeon-780m-64gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
197
+ { id: "radeon-780m-96gb", name: "Radeon 780M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
198
+ { id: "rtx-a5000-8gb", name: "RTX A5000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
199
+ { id: "rtx-a5000-12gb", name: "RTX A5000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 12 },
200
+ { id: "rtx-a5000-24gb", name: "RTX A5000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
201
+ { id: "mi50-16gb", name: "MI50", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 16 },
202
+ { id: "mi50-32gb", name: "MI50", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 32 },
203
+ { id: "rtx-4090-mobile", name: "RTX 4090 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
204
+ { id: "rtx-3080-mobile-8gb", name: "RTX 3080 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
205
+ { id: "rtx-3080-mobile-16gb", name: "RTX 3080 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
206
+ { id: "arc-a750", name: "Arc A750", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 8 },
207
+ { id: "rtx-4080-mobile", name: "RTX 4080 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
208
+ { id: "rx-7700-xt", name: "RX 7700 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 12 },
209
+ { id: "a40", name: "A40", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
210
+ { id: "a10", name: "A10", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
211
+ { id: "b300", name: "B300", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 288 },
212
+ { id: "gtx-1070", name: "GTX 1070", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
213
+ { id: "rtx-4090-laptop", name: "RTX 4090 Laptop", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 16 },
214
+ { id: "rtx-5000-ada", name: "RTX 5000 Ada", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 32 },
215
+ { id: "radeon-pro-vii-16gb", name: "Radeon Pro VII", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
216
+ { id: "radeon-pro-vii-32gb", name: "Radeon Pro VII", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
217
+ { id: "rtx-4090d-24gb", name: "RTX 4090D", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 24 },
218
+ { id: "rtx-4090d-48gb", name: "RTX 4090D", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 48 },
219
+ { id: "rtx-4080-laptop", name: "RTX 4080 Laptop", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 12 },
220
+ { id: "l40", name: "L40", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
221
+ { id: "rx-6650-xt", name: "RX 6650 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
222
+ { id: "jetson-agx-orin-64gb", name: "Jetson AGX Orin 64GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 64 },
223
+ { id: "gtx-1080", name: "GTX 1080", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
224
+ { id: "rtx-4500-ada", name: "RTX 4500 Ada", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
225
+ { id: "rtx-2060-12gb", name: "RTX 2060 12GB", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 12 },
226
+ { id: "rx-7900-gre", name: "RX 7900 GRE", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 16 },
227
+ { id: "rtx-5090-d", name: "RTX 5090 D", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 32 },
228
+ { id: "arc-b60-24gb", name: "Arc B60", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 24 },
229
+ { id: "arc-b60-48gb", name: "Arc B60", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 48 },
230
+ { id: "rtx-pro-5000-48gb", name: "RTX PRO 5000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
231
+ { id: "rtx-pro-5000-72gb", name: "RTX PRO 5000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 72 },
232
+ { id: "arc-b50", name: "Arc B50", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 16 },
233
+ { id: "rx-6700", name: "RX 6700", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 10 },
234
+ { id: "rtx-2050-mobile", name: "RTX 2050 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
235
+ { id: "quadro-p6000", name: "Quadro P6000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
236
+ { id: "mi300", name: "MI300", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 192 },
237
+ { id: "rtx-4000-sff-ada", name: "RTX 4000 SFF Ada", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 20 },
238
+ { id: "rtx-pro-4000", name: "RTX PRO 4000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
239
+ { id: "radeon-890m-16gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
240
+ { id: "radeon-890m-24gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
241
+ { id: "radeon-890m-32gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
242
+ { id: "radeon-890m-48gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
243
+ { id: "radeon-890m-64gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
244
+ { id: "radeon-890m-96gb", name: "Radeon 890M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
245
+ { id: "rx-5700-xt", name: "RX 5700 XT", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
246
+ { id: "apple-macbook-neo", name: "Apple MacBook Neo", vendor: "Apple", family: "Apple Silicon", backend: "metal", vram: 8 },
247
+ { id: "rtx-a2000-mobile-4gb", name: "RTX A2000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
248
+ { id: "rtx-a2000-mobile-8gb", name: "RTX A2000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
249
+ { id: "rtx-titan", name: "RTX Titan", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
250
+ { id: "rtx-5050", name: "RTX 5050", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
251
+ { id: "rtx-2070-super-mobile", name: "RTX 2070 SUPER Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
252
+ { id: "jetson-nano", name: "Jetson Nano", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 4 },
253
+ { id: "rtx-5050-mobile", name: "RTX 5050 Mobile", vendor: "NVIDIA", family: "RTX", backend: "cuda", vram: 8 },
254
+ { id: "rtx-pro-4500-ws", name: "RTX PRO 4500 WS", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 32 },
255
+ { id: "rtx-a3000-mobile-6gb", name: "RTX A3000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 6 },
256
+ { id: "rtx-a3000-mobile-12gb", name: "RTX A3000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 12 },
257
+ { id: "l20", name: "L20", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
258
+ { id: "jetson-orin-nx-16gb", name: "Jetson Orin NX 16GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 16 },
259
+ { id: "rx-5700", name: "RX 5700", vendor: "AMD", family: "AMD Radeon", backend: "rocm", vram: 8 },
260
+ { id: "rtx-pro-2000", name: "RTX PRO 2000", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
261
+ { id: "mi60", name: "MI60", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 32 },
262
+ { id: "a2", name: "A2", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
263
+ { id: "rtx-5880-ada", name: "RTX 5880 Ada", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 48 },
264
+ { id: "radeon-pro-v620", name: "Radeon Pro V620", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
265
+ { id: "arc-a380", name: "Arc A380", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 6 },
266
+ { id: "rtx-a5000-mobile", name: "RTX A5000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
267
+ { id: "radeon-860m-16gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
268
+ { id: "radeon-860m-24gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
269
+ { id: "radeon-860m-32gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
270
+ { id: "radeon-860m-48gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
271
+ { id: "radeon-860m-64gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
272
+ { id: "radeon-860m-96gb", name: "Radeon 860M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
273
+ { id: "arc-b570", name: "Arc B570", vendor: "INTEL", family: "Intel Arc", backend: "intel", vram: 10 },
274
+ { id: "mi250", name: "MI250", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 128 },
275
+ { id: "mi100", name: "MI100", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 32 },
276
+ { id: "rtx-pro-4000-sff", name: "RTX PRO 4000 SFF", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
277
+ { id: "radeon-610m-16gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
278
+ { id: "radeon-610m-24gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
279
+ { id: "radeon-610m-32gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
280
+ { id: "radeon-610m-48gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
281
+ { id: "radeon-610m-64gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
282
+ { id: "radeon-610m-96gb", name: "Radeon 610M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
283
+ { id: "mi210", name: "MI210", vendor: "AMD", family: "AMD Instinct", backend: "rocm", vram: 64 },
284
+ { id: "jetson-orin-nano-4gb", name: "Jetson Orin Nano 4GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 4 },
285
+ { id: "rtx-a4000-mobile", name: "RTX A4000 Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
286
+ { id: "jetson-agx-orin-32gb", name: "Jetson AGX Orin 32GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 32 },
287
+ { id: "jetson-agx-xavier-32gb", name: "Jetson AGX Xavier", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 32 },
288
+ { id: "jetson-agx-xavier-64gb", name: "Jetson AGX Xavier", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 64 },
289
+ { id: "t10", name: "T10", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
290
+ { id: "radeon-760m-16gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
291
+ { id: "radeon-760m-24gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
292
+ { id: "radeon-760m-32gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
293
+ { id: "radeon-760m-48gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
294
+ { id: "radeon-760m-64gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
295
+ { id: "radeon-760m-96gb", name: "Radeon 760M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
296
+ { id: "rtx-3500-ada-mobile", name: "RTX 3500 Ada Mobile", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 12 },
297
+ { id: "pro-w7800-32gb", name: "PRO W7800", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
298
+ { id: "pro-w7800-48gb", name: "PRO W7800", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
299
+ { id: "radeon-680m-16gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
300
+ { id: "radeon-680m-24gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
301
+ { id: "radeon-680m-32gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
302
+ { id: "radeon-680m-48gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
303
+ { id: "radeon-680m-64gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
304
+ { id: "radeon-680m-96gb", name: "Radeon 680M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
305
+ { id: "a30", name: "A30", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 24 },
306
+ { id: "radeon-880m-16gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
307
+ { id: "radeon-880m-24gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
308
+ { id: "radeon-880m-32gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
309
+ { id: "radeon-880m-48gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
310
+ { id: "radeon-880m-64gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
311
+ { id: "radeon-880m-96gb", name: "Radeon 880M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
312
+ { id: "pro-w7900", name: "PRO W7900", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
313
+ { id: "h20", name: "H20", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 96 },
314
+ { id: "radeon-740m-16gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
315
+ { id: "radeon-740m-24gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
316
+ { id: "radeon-740m-32gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
317
+ { id: "radeon-740m-48gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
318
+ { id: "radeon-740m-64gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
319
+ { id: "radeon-740m-96gb", name: "Radeon 740M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
320
+ { id: "a800-40gb", name: "A800", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 40 },
321
+ { id: "a800-80gb", name: "A800", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 80 },
322
+ { id: "jetson-orin-nx-8gb", name: "Jetson Orin NX 8GB", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 8 },
323
+ { id: "jetson-xavier-nx-8gb", name: "Jetson Xavier NX", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 8 },
324
+ { id: "jetson-xavier-nx-16gb", name: "Jetson Xavier NX", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 16 },
325
+ { id: "rtx-a5000-max-q", name: "RTX A5000 Max-Q", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 16 },
326
+ { id: "jetson-tx2-4gb", name: "Jetson TX2", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 4 },
327
+ { id: "jetson-tx2-8gb", name: "Jetson TX2", vendor: "NVIDIA", family: "Jetson", backend: "cuda", vram: 8 },
328
+ { id: "h800", name: "H800", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 80 },
329
+ { id: "rtx-a4000-max-q", name: "RTX A4000 Max-Q", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
330
+ { id: "radeon-840m-16gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
331
+ { id: "radeon-840m-24gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
332
+ { id: "radeon-840m-32gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
333
+ { id: "radeon-840m-48gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
334
+ { id: "radeon-840m-64gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
335
+ { id: "radeon-840m-96gb", name: "Radeon 840M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
336
+ { id: "rtx-a2000-max-q-4gb", name: "RTX A2000 Max-Q", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
337
+ { id: "rtx-a2000-max-q-8gb", name: "RTX A2000 Max-Q", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 8 },
338
+ { id: "rtx-a2000-embedded", name: "RTX A2000 Embedded", vendor: "NVIDIA", family: "GPU", backend: "cuda", vram: 4 },
339
+ { id: "radeon-820m-16gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 16 },
340
+ { id: "radeon-820m-24gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 24 },
341
+ { id: "radeon-820m-32gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 32 },
342
+ { id: "radeon-820m-48gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 48 },
343
+ { id: "radeon-820m-64gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 64 },
344
+ { id: "radeon-820m-96gb", name: "Radeon 820M", vendor: "AMD", family: "GPU", backend: "rocm", vram: 96 },
345
+ ];
346
+
347
+ /** "A100 80GB" — how a configuration reads in the sentence. */
348
+ export function hardwareLabel(h: HardwareConfig): string {
349
+ return `${h.name} ${h.vram}GB`;
350
+ }
lib/utils.ts ADDED
@@ -0,0 +1 @@
 
 
1
+ export { cn } from "cn"
next.config.ts CHANGED
@@ -1,7 +1,9 @@
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
- /* config options here */
 
 
5
  };
6
 
7
  export default nextConfig;
 
1
  import type { NextConfig } from "next";
2
 
3
  const nextConfig: NextConfig = {
4
+ // Spaces runs the app from a Docker image; standalone bundles the server and
5
+ // only the node_modules it actually reaches.
6
+ output: "standalone",
7
  };
8
 
9
  export default nextConfig;
package-lock.json CHANGED
The diff for this file is too large to render. See raw diff
 
package.json CHANGED
@@ -9,9 +9,15 @@
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
 
 
 
 
12
  "next": "16.3.4",
13
  "react": "19.2.8",
14
- "react-dom": "19.2.8"
 
 
15
  },
16
  "devDependencies": {
17
  "@tailwindcss/postcss": "^4",
 
9
  "lint": "eslint"
10
  },
11
  "dependencies": {
12
+ "@base-ui/react": "^1.8.0",
13
+ "class-variance-authority": "^0.7.1",
14
+ "cn": "^0.2.6",
15
+ "lucide-react": "^1.42.0",
16
  "next": "16.3.4",
17
  "react": "19.2.8",
18
+ "react-dom": "19.2.8",
19
+ "shadcn": "^4.21.0",
20
+ "tw-animate-css": "^1.4.0"
21
  },
22
  "devDependencies": {
23
  "@tailwindcss/postcss": "^4",