Spaces:
Running on CPU Upgrade
Build the quantization picker
Browse filesAn 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 +11 -0
- .dockerignore +5 -0
- Dockerfile +29 -0
- README.md +83 -22
- app/api/model/route.ts +99 -0
- app/compare/page.tsx +97 -0
- app/globals.css +164 -14
- app/layout.tsx +7 -16
- app/page.tsx +72 -63
- components.json +25 -0
- components/CapabilityRow.tsx +44 -0
- components/MemoryBar.tsx +228 -0
- components/ModelInput.tsx +84 -0
- components/SentenceCombobox.tsx +75 -0
- components/SentenceSelect.tsx +77 -0
- components/TradeoffMap.tsx +124 -0
- components/ui/button.tsx +57 -0
- components/ui/combobox.tsx +297 -0
- components/ui/input-group.tsx +158 -0
- components/ui/input.tsx +19 -0
- components/ui/textarea.tsx +17 -0
- lib/data.ts +252 -0
- lib/hardware.ts +350 -0
- lib/utils.ts +1 -0
- next.config.ts +3 -1
- package-lock.json +0 -0
- package.json +7 -1
|
@@ -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 |
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
node_modules
|
| 2 |
+
.next
|
| 3 |
+
.git
|
| 4 |
+
.claude
|
| 5 |
+
npm-debug.log
|
|
@@ -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"]
|
|
@@ -1,36 +1,97 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
#
|
| 4 |
|
| 5 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
|
| 23 |
-
#
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
|
| 32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
-
|
| 35 |
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -1,26 +1,176 @@
|
|
| 1 |
@import "tailwindcss";
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
:
|
| 4 |
-
--background: #ffffff;
|
| 5 |
-
--foreground: #171717;
|
| 6 |
-
}
|
| 7 |
|
| 8 |
@theme inline {
|
| 9 |
-
|
| 10 |
-
--color-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
--font-sans: var(--font-geist-sans);
|
| 12 |
--font-mono: var(--font-geist-mono);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
}
|
| 14 |
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
}
|
| 20 |
}
|
| 21 |
|
| 22 |
body {
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
}
|
|
@@ -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 |
-
|
| 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: "
|
| 17 |
-
description:
|
|
|
|
| 18 |
};
|
| 19 |
|
| 20 |
export default function RootLayout({ children }: LayoutProps<"/">) {
|
| 21 |
return (
|
| 22 |
-
<html
|
| 23 |
-
|
| 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 |
}
|
|
@@ -1,69 +1,78 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
-
export default function Home() {
|
| 4 |
return (
|
| 5 |
-
<
|
| 6 |
-
<
|
| 7 |
-
<
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
/>
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
<
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
</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 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
className="
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
|
|
|
| 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 |
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 }
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 }
|
|
@@ -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 }
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
export { cn } from "cn"
|
|
@@ -1,7 +1,9 @@
|
|
| 1 |
import type { NextConfig } from "next";
|
| 2 |
|
| 3 |
const nextConfig: NextConfig = {
|
| 4 |
-
/
|
|
|
|
|
|
|
| 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;
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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",
|