├── bun.lockb ├── db.sqlite ├── public ├── logo.png ├── banner.png ├── favicon.ico ├── mozilla-logo.png ├── apple-touch-icon.png ├── cli-screenshot.jpeg ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── manifest.webmanifest ├── accel-focus.svg ├── model-focus.svg ├── accel.svg ├── model.svg └── icon.svg ├── postcss.config.mjs ├── src ├── pages │ ├── download │ │ ├── TabStepLabel.tsx │ │ ├── TabStep.tsx │ │ ├── OperatingSystemSelector.tsx │ │ ├── ModelTab.tsx │ │ ├── index.tsx │ │ └── OfficialTab.tsx │ ├── _app.tsx │ ├── api │ │ ├── download │ │ │ └── [model].ts │ │ ├── search.ts │ │ └── results.ts │ ├── _document.tsx │ ├── index.tsx │ ├── model │ │ └── [id] │ │ │ └── index.tsx │ ├── latest.tsx │ ├── accelerator │ │ └── [id] │ │ │ └── index.tsx │ ├── about │ │ └── index.tsx │ └── result │ │ └── [id].tsx ├── lib │ ├── env.ts │ ├── swr.ts │ ├── constants.ts │ ├── hooks │ │ └── useDownload.ts │ ├── config.ts │ ├── utils.ts │ ├── types.ts │ └── selectStyles.ts ├── components │ ├── ui │ │ ├── CardHeader.tsx │ │ ├── Card.tsx │ │ ├── Button.tsx │ │ ├── Hyperlink.tsx │ │ ├── Separator.tsx │ │ ├── GenericSelect.tsx │ │ ├── Tooltip.tsx │ │ ├── CodeBlock.tsx │ │ ├── Tab.tsx │ │ └── GenericMultiSelect.tsx │ ├── layout │ │ ├── PageHeader.tsx │ │ ├── Layout.tsx │ │ ├── Meta.tsx │ │ ├── Footer.tsx │ │ ├── Header.tsx │ │ └── SearchBar.tsx │ ├── select │ │ ├── MultiSelectOption.tsx │ │ ├── CustomMenuList.tsx │ │ ├── ModelSelectOptionLabel.tsx │ │ └── AcceleratorSelectOptionLabel.tsx │ ├── icons │ │ ├── CaratIcon.tsx │ │ ├── ArrowIcon.tsx │ │ ├── EmailIcon.tsx │ │ ├── GithubIcon.tsx │ │ ├── DiscordIcon.tsx │ │ └── SearchIcon.tsx │ ├── leaderboard │ │ ├── constants.ts │ │ ├── LeaderboardAcceleratorSelect.tsx │ │ ├── LeaderboardSelectedModelHeader.tsx │ │ ├── Leaderboard.tsx │ │ ├── HomepageLeaderboard.tsx │ │ ├── LeaderboardAcceleratorRow.tsx │ │ ├── LeaderboardTable.tsx │ │ └── LeaderboardHeader.tsx │ ├── display │ │ ├── MetricSelector.tsx │ │ ├── RuntimeInfo.tsx │ │ ├── PerformanceMetricDisplay.tsx │ │ ├── PerformanceResultsGrid.tsx │ │ ├── SystemInfo.tsx │ │ ├── AcceleratorInfo.tsx │ │ └── ModelInfo.tsx │ ├── cards │ │ ├── compare │ │ │ ├── AcceleratorSelect.tsx │ │ │ ├── useInitialCompareSelection.ts │ │ │ ├── CompareCard.tsx │ │ │ ├── CompareCardComponents.tsx │ │ │ ├── AcceleratorCompareCard.tsx │ │ │ └── ModelCompareCard.tsx │ │ └── LatestRunCard.tsx │ ├── charts │ │ ├── AcceleratorMetricsChart.tsx │ │ ├── ModelMetricsChart.tsx │ │ └── MetricsBarChart.tsx │ └── informational │ │ └── TestDescriptionsTable.tsx ├── db │ ├── index.ts │ ├── schema.sql │ ├── schema.txt │ └── schema.ts ├── styles │ └── globals.css └── middleware.ts ├── .eslintrc.json ├── migrations ├── meta │ └── _journal.json └── 0000_dear_veda.sql ├── next.config.mjs ├── drizzle.config.ts ├── .gitignore ├── tsconfig.json ├── tailwind.config.ts ├── package.json └── README.md /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/bun.lockb -------------------------------------------------------------------------------- /db.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/db.sqlite -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/banner.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/mozilla-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/mozilla-logo.png -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/apple-touch-icon.png -------------------------------------------------------------------------------- /public/cli-screenshot.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/cli-screenshot.jpeg -------------------------------------------------------------------------------- /public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjpais/LocalScore/HEAD/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/pages/download/TabStepLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TabStepLabel = ({ children }: { children: React.ReactNode }) => { 4 | return
{children}
; 5 | }; 6 | 7 | export default TabStepLabel; 8 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next/typescript"], 3 | "rules": { 4 | "@typescript-eslint/ban-ts-comment": ["error", { 5 | "ts-ignore": "allow-with-description" 6 | }], 7 | "@typescript-eslint/no-explicit-any": "off" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "sqlite", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "6", 8 | "when": 1742420813556, 9 | "tag": "0000_dear_veda", 10 | "breakpoints": true 11 | } 12 | ] 13 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | rewrites: () => [ 5 | { 6 | source: '/download/:model', 7 | destination: '/api/download/:model' 8 | } 9 | ] 10 | }; 11 | 12 | export default nextConfig; 13 | -------------------------------------------------------------------------------- /public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "icons": [ 3 | { 4 | "src": "/android-chrome-192x192.png", 5 | "sizes": "192x192", 6 | "type": "image/png" 7 | }, 8 | { 9 | "src": "/android-chrome-512x512.png", 10 | "sizes": "512x512", 11 | "type": "image/png" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/pages/download/TabStep.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const TabStep = ({ 4 | children, 5 | className = "", 6 | }: { 7 | children: React.ReactNode; 8 | className?: string; 9 | }) => { 10 | return
{children}
; 11 | }; 12 | 13 | export default TabStep; 14 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import Layout from "@/components/layout/Layout"; 2 | import "@/styles/globals.css"; 3 | import type { AppProps } from "next/app"; 4 | 5 | export default function App({ Component, pageProps }: AppProps) { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/env.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import "dotenv/config"; 3 | 4 | const envSchema = z.object({ 5 | PGHOST: z.string(), 6 | PGDATABASE: z.string(), 7 | PGUSER: z.string(), 8 | PGPASSWORD: z.string(), 9 | PGPORT: z.coerce.number().default(5432), 10 | }); 11 | 12 | export const env = envSchema.parse(process.env); 13 | -------------------------------------------------------------------------------- /public/accel-focus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ui/CardHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CardHeader = ({ 4 | text, 5 | className, 6 | }: { 7 | text: string; 8 | className?: string; 9 | }) => { 10 | return ( 11 |
14 | {text} 15 |
16 | ); 17 | }; 18 | 19 | export default CardHeader; 20 | -------------------------------------------------------------------------------- /src/components/layout/PageHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Separator from "../ui/Separator"; 3 | 4 | const PageHeader = ({ children }: { children: React.ReactNode }) => { 5 | return ( 6 |
7 |
8 | {children} 9 |
10 | 11 |
12 | ); 13 | }; 14 | 15 | export default PageHeader; 16 | -------------------------------------------------------------------------------- /src/components/ui/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface CardProps { 4 | children: React.ReactNode; 5 | className?: string; 6 | } 7 | 8 | const Card: React.FC = ({ children, className = "" }) => { 9 | return ( 10 |
13 | {children} 14 |
15 | ); 16 | }; 17 | 18 | export default Card; 19 | -------------------------------------------------------------------------------- /src/lib/swr.ts: -------------------------------------------------------------------------------- 1 | export const fetcher = (url: string) => fetch(url).then((res) => res.json()); 2 | 3 | export const postFetcher = async (url: string, data: any) => { 4 | const response = await fetch(url, { 5 | method: "POST", 6 | headers: { 7 | "Content-Type": "application/json", 8 | }, 9 | body: JSON.stringify(data), 10 | }); 11 | 12 | if (!response.ok) { 13 | throw new Error("Failed to fetch data"); 14 | } 15 | 16 | return response.json(); 17 | }; 18 | -------------------------------------------------------------------------------- /src/components/select/MultiSelectOption.tsx: -------------------------------------------------------------------------------- 1 | import { components, OptionProps } from "react-select"; 2 | 3 | const MultiSelectOption: React.FC> = (props) => ( 4 | 5 |
6 |
{props.isSelected ? "✓ " : ""}
7 |
{props.children}
8 |
9 |
10 | ); 11 | 12 | export default MultiSelectOption; 13 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "drizzle-kit"; 2 | import * as dotenv from "dotenv"; 3 | 4 | // Load .env.local file 5 | dotenv.config({ path: ".env.local" }); 6 | 7 | export default defineConfig({ 8 | out: "./migrations", 9 | schema: "./src/db/schema.ts", 10 | dialect: "turso", 11 | dbCredentials: { 12 | url: process.env.TURSO_DATABASE_URL 13 | ? process.env.TURSO_DATABASE_URL 14 | : "file:db.sqlite", 15 | authToken: process.env.TURSO_AUTH_TOKEN, 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /src/db/index.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from "drizzle-orm/libsql"; 2 | import { createClient as createTursoClient } from "@libsql/client/web"; 3 | import { createClient as createNodeClient } from "@libsql/client/node"; 4 | 5 | const dbClient = process.env.TURSO_DATABASE_URL 6 | ? createTursoClient({ 7 | url: process.env.TURSO_DATABASE_URL, 8 | authToken: process.env.TURSO_AUTH_TOKEN, 9 | }) 10 | : createNodeClient({ 11 | url: "file:db.sqlite", 12 | }); 13 | 14 | const db = drizzle({ client: dbClient }); 15 | 16 | export default db; 17 | -------------------------------------------------------------------------------- /public/model-focus.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/icons/CaratIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const CaratIcon = ({ 4 | strokeWidth = 3, 5 | className = "", 6 | stroke = "#582acb", 7 | }: { 8 | strokeWidth?: number; 9 | className?: string; 10 | stroke?: string; 11 | }) => { 12 | return ( 13 | 24 | 25 | 26 | ); 27 | }; 28 | 29 | export default CaratIcon; 30 | -------------------------------------------------------------------------------- /src/components/select/CustomMenuList.tsx: -------------------------------------------------------------------------------- 1 | import { MenuListProps, components } from "react-select"; 2 | import React from "react"; 3 | 4 | const MenuListWithHeader: React.FC< 5 | MenuListProps & { headerText: string } 6 | > = (props) => { 7 | return ( 8 | 9 |
15 | {props.headerText} 16 |
17 | {props.children} 18 |
19 | ); 20 | }; 21 | 22 | export default MenuListWithHeader; 23 | -------------------------------------------------------------------------------- /src/components/ui/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { ButtonHTMLAttributes } from "react"; 2 | 3 | interface ButtonProps extends ButtonHTMLAttributes { 4 | className?: string; 5 | children: React.ReactNode; 6 | } 7 | 8 | const Button: React.FC = ({ 9 | className = "", 10 | children, 11 | ...props 12 | }) => { 13 | const defaultClasses = 14 | "px-4 py-2 bg-primary-100 text-primary-500 hover:text-white hover:bg-primary-500 rounded"; 15 | const combinedClasses = className 16 | ? `${defaultClasses} ${className}` 17 | : defaultClasses; 18 | 19 | return ( 20 | 23 | ); 24 | }; 25 | 26 | export default Button; 27 | -------------------------------------------------------------------------------- /src/components/leaderboard/constants.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey } from "@/lib/types"; 2 | 3 | export interface LeaderboardColumn { 4 | key: PerformanceMetricKey; 5 | label: string; 6 | sortable?: boolean; 7 | className?: string; 8 | } 9 | export const LEADERBOARD_COLUMNS: LeaderboardColumn[] = [ 10 | { 11 | key: "avg_prompt_tps", 12 | label: "PROMPT", 13 | sortable: true, 14 | }, 15 | { 16 | key: "avg_gen_tps", 17 | label: "GENERATION", 18 | sortable: true, 19 | }, 20 | { 21 | key: "avg_ttft", 22 | label: "TTFT", 23 | sortable: true, 24 | }, 25 | { 26 | key: "performance_score", 27 | label: "LOCALSCORE", 28 | sortable: true, 29 | className: "font-bold", 30 | }, 31 | ]; 32 | -------------------------------------------------------------------------------- /src/components/layout/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import Header from "@/components/layout/Header"; 3 | import Footer from "./Footer"; 4 | 5 | type LayoutProps = { 6 | children: ReactNode; 7 | }; 8 | 9 | export default function Layout({ children }: LayoutProps) { 10 | return ( 11 |
12 |
13 |
14 |
15 |
16 |
17 | {children} 18 |
19 |
20 |
21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/constants.ts: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey, SortDirection } from "./types"; 2 | 3 | export const MetricLabels: Record = { 4 | avg_prompt_tps: "Prompt tokens/s", 5 | avg_gen_tps: "Generation tokens/s", 6 | avg_ttft: "Time to First Token (ms)", 7 | performance_score: "LocalScore", 8 | }; 9 | 10 | export const MetricUnits: Record = { 11 | avg_prompt_tps: "tokens/s", 12 | avg_gen_tps: "tokens/s", 13 | avg_ttft: "ms", 14 | performance_score: "LocalScore", 15 | }; 16 | 17 | export const MetricSortDirection: Record = 18 | { 19 | avg_prompt_tps: "desc", 20 | avg_gen_tps: "desc", 21 | avg_ttft: "asc", 22 | performance_score: "desc", 23 | }; 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": [ 4 | "dom", 5 | "dom.iterable", 6 | "esnext" 7 | ], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "noEmit": true, 12 | "esModuleInterop": true, 13 | "module": "esnext", 14 | "moduleResolution": "bundler", 15 | "resolveJsonModule": true, 16 | "isolatedModules": true, 17 | "jsx": "preserve", 18 | "incremental": true, 19 | "paths": { 20 | "@/*": [ 21 | "./src/*" 22 | ] 23 | }, 24 | "plugins": [ 25 | { 26 | "name": "next" 27 | } 28 | ] 29 | }, 30 | "include": [ 31 | "next-env.d.ts", 32 | "**/*.ts", 33 | "**/*.tsx", 34 | ".next/types/**/*.ts" 35 | ], 36 | "exclude": [ 37 | "node_modules" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/components/leaderboard/LeaderboardAcceleratorSelect.tsx: -------------------------------------------------------------------------------- 1 | import { AcceleratorType } from "@/lib/types"; 2 | import GenericSelect from "../ui/GenericSelect"; 3 | 4 | const LeaderboardAcceleratorSelect = ({ 5 | onChange, 6 | }: { 7 | onChange: (value: AcceleratorType) => void; 8 | }) => { 9 | const acceleratorOptions: { value: AcceleratorType; label: string }[] = [ 10 | { value: "GPU", label: "GPU" }, 11 | { value: "ALL", label: "CPU+GPU" }, 12 | { value: "CPU", label: "CPU" }, 13 | ]; 14 | 15 | return ( 16 |
17 | 23 |
24 | ); 25 | }; 26 | 27 | export default LeaderboardAcceleratorSelect; 28 | -------------------------------------------------------------------------------- /src/components/select/ModelSelectOptionLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | import { Model } from "@/lib/types"; 4 | 5 | const ModelSelectOptionLabel = ({ 6 | model, 7 | isFocused, 8 | }: { 9 | model: Model; 10 | isFocused: boolean; 11 | }) => { 12 | return ( 13 |
14 |
15 | a small icon of a model 21 |

{model.name}

22 |
23 |

{model.quant}

24 |
25 | ); 26 | }; 27 | 28 | export default ModelSelectOptionLabel; 29 | -------------------------------------------------------------------------------- /src/components/ui/Hyperlink.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import React, { ReactNode } from "react"; 3 | 4 | const Hyperlink = ({ 5 | href, 6 | children, 7 | className = "", 8 | variant = "primary", 9 | }: { 10 | href: string; 11 | children: ReactNode; 12 | className?: string; 13 | variant?: "primary" | "button"; 14 | }) => { 15 | const primaryClasses = "text-primary-500 hover:underline"; 16 | const buttonClasses = 17 | "px-4 py-2 bg-primary-100 text-primary-500 hover:text-white hover:bg-primary-500 rounded"; 18 | 19 | const classes = 20 | variant === "primary" 21 | ? `${primaryClasses} ${className}` 22 | : `${buttonClasses} ${className}`; 23 | 24 | return ( 25 | 26 | {children} 27 | 28 | ); 29 | }; 30 | 31 | export default Hyperlink; 32 | -------------------------------------------------------------------------------- /src/components/icons/ArrowIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const ArrowIcon = ({ 4 | className = "", 5 | color = "#000", 6 | direction = "down", 7 | }: { 8 | className?: string; 9 | color?: string; 10 | direction?: "up" | "down"; 11 | }) => { 12 | const rotate = direction === "up" ? "rotate(180deg)" : "none"; 13 | 14 | return ( 15 | 24 | 28 | 29 | ); 30 | }; 31 | 32 | export default ArrowIcon; 33 | -------------------------------------------------------------------------------- /public/accel.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/components/layout/Meta.tsx: -------------------------------------------------------------------------------- 1 | import Head from "next/head"; 2 | 3 | type MetaProps = { 4 | title?: string; 5 | description?: string; 6 | ogImage?: string; 7 | noindex?: boolean; 8 | }; 9 | 10 | export default function Meta({ 11 | title = "LocalScore - Local AI Benchmark", 12 | description = "LocalScore is an open benchmark which helps you understand how well your computer can handle local AI tasks.", 13 | ogImage = "/og-image.png", 14 | noindex = false, 15 | }: MetaProps) { 16 | return ( 17 | 18 | {title} 19 | 20 | 21 | 22 | {ogImage && } 23 | {noindex && } 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ui/Separator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SeparatorProps { 4 | thickness?: number; 5 | color?: string; 6 | className?: string; 7 | direction?: "horizontal" | "vertical"; 8 | } 9 | 10 | const Separator = ({ 11 | thickness = 1, 12 | color = "#EEECF5", 13 | className = "", 14 | direction = "horizontal", 15 | }: SeparatorProps) => { 16 | const style: React.CSSProperties = 17 | direction === "horizontal" 18 | ? { 19 | borderTopWidth: `${thickness}px`, 20 | borderTopStyle: "solid", 21 | borderTopColor: color, 22 | } 23 | : { 24 | borderLeftWidth: `${thickness}px`, 25 | borderLeftStyle: "solid", 26 | borderLeftColor: color, 27 | height: "100%", 28 | }; 29 | 30 | return
; 31 | }; 32 | 33 | export default Separator; 34 | -------------------------------------------------------------------------------- /src/components/display/MetricSelector.tsx: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey, sortableResultKeys } from "@/lib/types"; 2 | import GenericSelect from "../ui/GenericSelect"; 3 | import { MetricLabels } from "@/lib/constants"; 4 | 5 | interface MetricSelectorProps { 6 | selectedKey: PerformanceMetricKey; 7 | onChange?: (key: PerformanceMetricKey) => void; 8 | } 9 | 10 | const MetricSelector: React.FC = ({ 11 | selectedKey, 12 | onChange, 13 | }) => { 14 | const options = sortableResultKeys.map((key) => ({ 15 | value: key, 16 | // TODO refactor this is odd 17 | label: MetricLabels[key], 18 | })); 19 | 20 | return ( 21 |
22 | onChange?.(v)} 26 | className="font-semibold" 27 | /> 28 |
29 | ); 30 | }; 31 | 32 | export default MetricSelector; 33 | -------------------------------------------------------------------------------- /src/pages/api/download/[model].ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | // Define model URLs (replace with your actual Hugging Face links) 4 | const MODEL_URLS: Record = { 5 | "localscore-tiny": 6 | "https://huggingface.co/Mozilla/LocalScore/resolve/main/localscore-tiny-1b", 7 | "localscore-small": 8 | "https://huggingface.co/Mozilla/LocalScore/resolve/main/localscore-small-8b", 9 | "localscore-medium": 10 | "https://huggingface.co/Mozilla/LocalScore/resolve/main/localscore-medium-14b", 11 | }; 12 | 13 | export default function handler(req: NextApiRequest, res: NextApiResponse) { 14 | const { model } = req.query as { model: string }; 15 | 16 | // Validate the model and redirect 17 | if (MODEL_URLS[model]) { 18 | res.redirect(307, MODEL_URLS[model]); 19 | } else { 20 | res 21 | .status(404) 22 | .json({ error: "Model not found. Valid options: tiny, small, medium" }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 18 | 24 | 25 | 26 | 27 | 28 | 29 | 30 |
31 | 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/lib/hooks/useDownload.ts: -------------------------------------------------------------------------------- 1 | import { OFFICIAL_MODELS } from "@/lib/config"; 2 | import { OperatingSystem } from "@/lib/types"; 3 | import { create } from "zustand"; 4 | 5 | interface DownloadState { 6 | // State 7 | selectedModelIndex: number; 8 | selectedModel: (typeof OFFICIAL_MODELS)[number]; 9 | models: typeof OFFICIAL_MODELS; 10 | operatingSystem: OperatingSystem; 11 | 12 | // Actions 13 | setSelectedModelIndex: (index: number) => void; 14 | setOperatingSystem: (os: OperatingSystem) => void; 15 | } 16 | 17 | export const useDownloadStore = create((set) => ({ 18 | // Initial state 19 | selectedModelIndex: 0, 20 | selectedModel: OFFICIAL_MODELS[0], 21 | models: OFFICIAL_MODELS, 22 | operatingSystem: "MacOS/Linux", 23 | 24 | // Actions 25 | setSelectedModelIndex: (index: number) => 26 | set({ selectedModelIndex: index, selectedModel: OFFICIAL_MODELS[index] }), 27 | setOperatingSystem: (os: OperatingSystem) => set({ operatingSystem: os }), 28 | })); 29 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Fira+Mono:wght@400;500;600;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"); 2 | 3 | @tailwind base; 4 | @tailwind components; 5 | @tailwind utilities; 6 | 7 | :root { 8 | --background: #f9f7fa; 9 | --background-menu: #f1edfc; 10 | --background-menu-hover: #e2dafc; 11 | --background-scrollbar: #e9e6f8; 12 | 13 | --scrollbar-thumb: #b9a1fc; 14 | 15 | --primary-10: #e6dfff40; 16 | --primary-50: #e6dfff66; 17 | --primary-100: #e6dfff; 18 | --primary-200: #d3c7ff; 19 | --primary-500: #582acb; 20 | 21 | --grey-400: #bab4d9; 22 | } 23 | 24 | body { 25 | color: var(--foreground); 26 | background: var(--background); 27 | font-family: "Fira Sans", system-ui, sans-serif; 28 | } 29 | 30 | @layer utilities { 31 | .text-balance { 32 | text-wrap: balance; 33 | } 34 | } 35 | 36 | html { 37 | scrollbar-gutter: stable; 38 | } -------------------------------------------------------------------------------- /src/components/select/AcceleratorSelectOptionLabel.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Image from "next/image"; 3 | import { Accelerator } from "@/lib/types"; 4 | 5 | const AcceleratorSelectOptionLabel = ({ 6 | acc, 7 | isFocused, 8 | }: { 9 | acc: Accelerator; 10 | isFocused: boolean; 11 | }) => { 12 | const textClasses = "font-light sm:text-sm text-xs"; 13 | 14 | return ( 15 |
16 |
17 | a small icon of a computer chip 23 |

{acc.name}

24 |
25 |
26 |

{acc.memory_gb}GB

27 |

28 |

{acc.type}

29 |
30 |
31 | ); 32 | }; 33 | 34 | export default AcceleratorSelectOptionLabel; 35 | -------------------------------------------------------------------------------- /src/components/leaderboard/LeaderboardSelectedModelHeader.tsx: -------------------------------------------------------------------------------- 1 | import { PerformanceScore } from "@/lib/types"; 2 | import Link from "next/link"; 3 | import React from "react"; 4 | import Image from "next/image"; 5 | 6 | const LeaderboardSelectedModelHeader = ({ 7 | data, 8 | }: { 9 | data: PerformanceScore | undefined; 10 | }) => { 11 | if (!data) return null; 12 | 13 | return ( 14 |
15 | model icon 16 |
17 | 21 | 22 | {data.model.name}{" "} 23 | {data.model.quant} 24 | 25 | 26 |
27 |
28 | ); 29 | }; 30 | 31 | export default LeaderboardSelectedModelHeader; 32 | -------------------------------------------------------------------------------- /public/model.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/icons/EmailIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EmailIcon = ({ 4 | width = 24, 5 | height = 24, 6 | className = "", 7 | }: { 8 | width?: number; 9 | height?: number; 10 | className?: string; 11 | }) => { 12 | return ( 13 | <> 14 |
15 | Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com 16 | License - https://fontawesome.com/license/free Copyright 2025 Fonticons, 17 | Inc. 18 |
19 | 20 | 27 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default EmailIcon; 34 | -------------------------------------------------------------------------------- /src/components/display/RuntimeInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Runtime } from "@/lib/types"; 2 | import React from "react"; 3 | 4 | interface RuntimeInfoProps { 5 | runtime: Runtime; 6 | } 7 | 8 | const RuntimeInfo: React.FC = ({ runtime }) => { 9 | return ( 10 |
11 |
12 |
Name
13 |
{runtime.name}
14 |
15 | 16 | {runtime.version && ( 17 |
18 |
Version
19 |
{runtime.version}
20 |
21 | )} 22 | 23 | {runtime.commit_hash && ( 24 |
25 |
Commit Hash
26 |
{runtime.commit_hash}
27 |
28 | )} 29 |
30 | ); 31 | }; 32 | 33 | export default RuntimeInfo; 34 | -------------------------------------------------------------------------------- /src/pages/download/OperatingSystemSelector.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useDownloadStore } from "../../lib/hooks/useDownload"; 3 | 4 | const OperatingSystemSelector = () => { 5 | const { operatingSystem, setOperatingSystem } = useDownloadStore(); 6 | 7 | const baseStyles = "font-medium rounded-lg px-5 py-[10px]"; 8 | const activeStyles = "border-2 border-primary-500"; 9 | 10 | const isWindows = operatingSystem === "Windows"; 11 | 12 | return ( 13 |
14 | 24 | 34 |
35 | ); 36 | }; 37 | 38 | export default OperatingSystemSelector; 39 | -------------------------------------------------------------------------------- /src/components/leaderboard/Leaderboard.tsx: -------------------------------------------------------------------------------- 1 | import { AcceleratorType, PerformanceScore } from "@/lib/types"; 2 | import { useState } from "react"; 3 | import Card from "../ui/Card"; 4 | import CardHeader from "../ui/CardHeader"; 5 | import LeaderboardTable from "./LeaderboardTable"; 6 | import LeaderboardAcceleratorSelect from "./LeaderboardAcceleratorSelect"; 7 | 8 | interface LeaderboardProps { 9 | data: PerformanceScore[]; 10 | } 11 | 12 | const Leaderboard = ({ data }: LeaderboardProps) => { 13 | const [filterType, setFilterType] = useState("GPU"); 14 | 15 | const modelToUse = data[0]?.model; 16 | const selectedModelData = data.find((d) => d.model.name === modelToUse?.name); 17 | 18 | return ( 19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 | 27 | 28 |
29 | ); 30 | }; 31 | 32 | export default Leaderboard; 33 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server"; 2 | import type { NextRequest } from "next/server"; 3 | 4 | export function middleware(request: NextRequest) { 5 | // Skip all checks in development mode 6 | if (process.env.NODE_ENV === "development") { 7 | return NextResponse.next(); 8 | } 9 | 10 | // Regular middleware logic for production 11 | const referer = request.headers.get("referer"); 12 | const origin = request.headers.get("origin"); 13 | 14 | const allowedOrigins = [ 15 | "https://localscore.org", 16 | "https://www.localscore.org", 17 | "https://localscore.ai", 18 | "https://www.localscore.ai", 19 | "https://localscore.vercel.app", 20 | ]; 21 | 22 | if (process.env.VERCEL_URL) { 23 | allowedOrigins.push(`https://${process.env.VERCEL_URL}`); 24 | } 25 | 26 | const sourceOrigin = referer || origin; 27 | if ( 28 | !sourceOrigin || 29 | !allowedOrigins.some((origin) => sourceOrigin.startsWith(origin)) 30 | ) { 31 | return new NextResponse(JSON.stringify({ error: "Access forbidden" }), { 32 | status: 403, 33 | headers: { 34 | "Content-Type": "application/json", 35 | }, 36 | }); 37 | } 38 | 39 | return NextResponse.next(); 40 | } 41 | 42 | export const config = { 43 | matcher: "/api/search", 44 | }; 45 | -------------------------------------------------------------------------------- /src/components/icons/GithubIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const GithubIcon = ({ 4 | width = 24, 5 | height = 24, 6 | className = "fill-[#24292f]", 7 | }: { 8 | width?: number; 9 | height?: number; 10 | className?: string; 11 | }) => { 12 | return ( 13 | 20 | 25 | 26 | ); 27 | }; 28 | 29 | export default GithubIcon; 30 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | colors: { 12 | background: "var(--background)", 13 | foreground: "var(--foreground)", 14 | primary: { 15 | 10: "var(--primary-10)", 16 | 50: "var(--primary-50)", 17 | 100: "var(--primary-100)", 18 | 200: "var(--primary-200)", 19 | 500: "var(--primary-500)", 20 | }, 21 | grey: { 22 | 400: "var(--grey-400)", 23 | }, 24 | }, 25 | fontSize: { 26 | "heading-xxl": ["4.75rem", "4.75rem"], // 76px, 100% 27 | "heading-xl": ["4.125rem", "4.125rem"], // 66px, 100% 28 | "heading-lg": ["3.5rem", "3.5rem"], // 56px, 100% 29 | "heading-md": ["3rem", "3rem"], // 48px, 100% 30 | "heading-sm": ["2.375rem", "40px"], // 38px, 105% 31 | "heading-xs": ["1.75rem", "30px"], // 28px, 107% 32 | "heading-xxs": ["1.5rem", "26px"], // 24px, 108% 33 | "heading-xxxs": ["1.125rem", "20px"], // 18px, 111% 34 | }, 35 | fontFamily: { 36 | zilla: ["Zilla Slab", "serif"], 37 | mono: ["Fira Mono", "monospace"], 38 | }, 39 | }, 40 | }, 41 | plugins: [], 42 | }; 43 | export default config; 44 | -------------------------------------------------------------------------------- /src/components/cards/compare/AcceleratorSelect.tsx: -------------------------------------------------------------------------------- 1 | import AcceleratorSelectOptionLabel from "@/components/select/AcceleratorSelectOptionLabel"; 2 | import GenericMultiSelect from "@/components/ui/GenericMultiSelect"; 3 | import { Accelerator, UniqueAccelerator } from "@/lib/types"; 4 | 5 | interface AcceleratorSelectProps { 6 | accelerators: Accelerator[]; 7 | onChange: (selectedAccelerators: Accelerator[]) => void; 8 | defaultValue?: UniqueAccelerator[]; 9 | } 10 | 11 | const AcceleratorSelect: React.FC = ({ 12 | accelerators, 13 | onChange, 14 | defaultValue = [], 15 | }) => { 16 | return ( 17 | 25 | acc.name === uniqueAcc.name && acc.memory_gb === uniqueAcc.memory 26 | } 27 | renderOptionLabel={(acc, isFocused) => ( 28 | 29 | )} 30 | renderMultiValueLabel={(acc) => ( 31 |
32 |

{acc.name}

33 |

{acc.memory_gb}GB

34 |
35 | )} 36 | /> 37 | ); 38 | }; 39 | 40 | export default AcceleratorSelect; 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "llamascore", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "drizzle-kit generate", 11 | "db:migrate": "drizzle-kit migrate", 12 | "db:studio": "drizzle-kit studio" 13 | }, 14 | "dependencies": { 15 | "@as-integrations/next": "^3.2.0", 16 | "@libsql/client": "^0.15.1", 17 | "@types/chroma-js": "^2.4.5", 18 | "@types/react-responsive": "^9.0.0", 19 | "chroma-js": "^3.1.2", 20 | "dayjs": "^1.11.13", 21 | "dotenv": "^16.4.7", 22 | "drizzle-graphql": "^0.8.5", 23 | "drizzle-orm": "^0.36.4", 24 | "graphql": "^16.10.0", 25 | "graphql-yoga": "^5.13.2", 26 | "next": "14.2.35", 27 | "pg": "^8.14.1", 28 | "postgres": "^3.4.5", 29 | "react": "^18.3.1", 30 | "react-dom": "^18.3.1", 31 | "react-responsive": "^10.0.1", 32 | "react-select": "^5.10.1", 33 | "recharts": "^2.15.1", 34 | "swr": "^2.3.3", 35 | "uuid": "^11.1.0", 36 | "zod": "^3.24.2", 37 | "zustand": "^5.0.3" 38 | }, 39 | "devDependencies": { 40 | "@types/node": "^20.17.27", 41 | "@types/pg": "^8.11.11", 42 | "@types/react": "^18.3.20", 43 | "@types/react-dom": "^18.3.5", 44 | "drizzle-kit": "^0.27.2", 45 | "eslint": "^8.57.1", 46 | "eslint-config-next": "14.2.16", 47 | "postcss": "^8.5.3", 48 | "tailwindcss": "^3.4.17", 49 | "tsx": "^4.19.3", 50 | "typescript": "^5.8.2" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/display/PerformanceMetricDisplay.tsx: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey } from "@/lib/types"; 2 | import { formatMetricValue } from "@/lib/utils"; 3 | 4 | interface MetricDisplayProps { 5 | label: string; 6 | metricKey: PerformanceMetricKey; 7 | value: number; 8 | size?: "small" | "large" | "xl"; 9 | } 10 | 11 | const PerformanceMetricDisplay: React.FC = ({ 12 | label, 13 | metricKey, 14 | value, 15 | size = "small", 16 | }) => { 17 | const { formatted, suffix } = formatMetricValue(metricKey, value); 18 | 19 | const sizeStyles = { 20 | small: { 21 | container: "gap-1.5", 22 | value: "text-lg", 23 | suffix: "text-xs", 24 | label: "text-xs", 25 | }, 26 | large: { 27 | container: "gap-2", 28 | value: "text-xl", 29 | suffix: "text-sm", 30 | label: "text-sm", 31 | }, 32 | xl: { 33 | container: "sm:gap-3 gap-1", 34 | value: "text-xl sm:text-3xl", 35 | suffix: "text-sm sm:text-lg", 36 | label: "text-xs sm:text-lg", 37 | }, 38 | }; 39 | 40 | const styles = sizeStyles[size]; 41 | 42 | return ( 43 |
44 |
45 |

{formatted}

46 |

{suffix}

47 |
48 |

{label}

49 |
50 | ); 51 | }; 52 | 53 | export default PerformanceMetricDisplay; 54 | -------------------------------------------------------------------------------- /src/components/icons/DiscordIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const DiscordIcon = ({ 4 | width = 24, 5 | height = 24, 6 | className = "", 7 | }: { 8 | width?: number; 9 | height?: number; 10 | className?: string; 11 | }) => { 12 | return ( 13 | 26 | ); 27 | }; 28 | 29 | export default DiscordIcon; 30 | -------------------------------------------------------------------------------- /src/components/cards/compare/useInitialCompareSelection.ts: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | 3 | interface InitializeSelectionOptions { 4 | allItems: T[]; 5 | officialItems: U[]; 6 | defaultCount: number; 7 | itemMatchFn: (item: T, officialItem: U) => boolean; 8 | mapFn: (item: T) => U; 9 | } 10 | 11 | export function useInitialCompareSelection({ 12 | allItems, 13 | officialItems, 14 | defaultCount, 15 | itemMatchFn, 16 | mapFn, 17 | }: InitializeSelectionOptions) { 18 | const getInitialItems = () => { 19 | if (!allItems.length) return []; 20 | 21 | // First, collect all official items that exist in the results 22 | const availableOfficialItems = officialItems.filter((officialItem) => 23 | allItems.some((item) => itemMatchFn(item, officialItem)) 24 | ); 25 | 26 | // If we already have enough official items, use them 27 | if (availableOfficialItems.length >= defaultCount) { 28 | return availableOfficialItems; 29 | } 30 | 31 | // Otherwise, add non-official items to reach the minimum 32 | const nonOfficialItems = allItems 33 | .filter( 34 | (item) => 35 | !availableOfficialItems.some((officialItem) => 36 | itemMatchFn(item, officialItem) 37 | ) 38 | ) 39 | .slice(0, defaultCount - availableOfficialItems.length) 40 | .map(mapFn); 41 | 42 | // Combine official and additional items 43 | return [...availableOfficialItems, ...nonOfficialItems]; 44 | }; 45 | 46 | const [selectedItems, setSelectedItems] = useState(getInitialItems()); 47 | 48 | return { selectedItems, setSelectedItems }; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ui/GenericSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CaratIcon from "../icons/CaratIcon"; 3 | 4 | interface SelectProps { 5 | options: Array<{ value: T; label: string }>; 6 | onChange: (value: T) => void; 7 | defaultValue?: T; 8 | className?: string; 9 | roundedStyle?: "left" | "right" | "both" | "none"; 10 | width?: string; 11 | } 12 | 13 | const GenericSelect = ({ 14 | options, 15 | onChange, 16 | defaultValue, 17 | className = "", 18 | roundedStyle = "both", 19 | }: SelectProps) => { 20 | const getRoundedClass = () => { 21 | switch (roundedStyle) { 22 | case "left": 23 | return "rounded-l-md"; 24 | case "right": 25 | return "rounded-r-md"; 26 | case "both": 27 | return "rounded-md"; 28 | case "none": 29 | return ""; 30 | default: 31 | return "rounded-md"; 32 | } 33 | }; 34 | 35 | return ( 36 | <> 37 | 48 | 49 | 50 | ); 51 | }; 52 | 53 | export default GenericSelect; 54 | -------------------------------------------------------------------------------- /src/components/ui/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | 3 | interface TooltipProps { 4 | text: string; 5 | className?: string; 6 | tooltipClassName?: string; 7 | } 8 | 9 | const Tooltip: React.FC = ({ 10 | text, 11 | className = "", 12 | tooltipClassName = "", 13 | }) => { 14 | const [isVisible, setIsVisible] = useState(false); 15 | 16 | const showTooltip = () => setIsVisible(true); 17 | const hideTooltip = () => setIsVisible(false); 18 | 19 | return ( 20 |
21 | {/* Tooltip trigger - info icon */} 22 | 33 | 34 | {/* Tooltip content */} 35 | {isVisible && ( 36 |
40 | {/* Arrow */} 41 |
42 | {text} 43 |
44 | )} 45 |
46 | ); 47 | }; 48 | 49 | export default Tooltip; 50 | -------------------------------------------------------------------------------- /src/components/display/PerformanceResultsGrid.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PerformanceMetricDisplay from "./PerformanceMetricDisplay"; 3 | import { Run } from "@/lib/types"; 4 | 5 | const PerformanceResultsGrid = ({ 6 | run, 7 | size = "small", 8 | className = "", 9 | }: { 10 | run: Run; 11 | size?: "small" | "large" | "xl"; 12 | className?: string; 13 | }) => { 14 | // Common classes for all sizes 15 | const commonClasses = "grid items-center"; 16 | 17 | // Size-specific classes 18 | const sizeClasses = { 19 | small: 20 | "grid-cols-2 sm:gap-2 gap-x-16 gap-y-2 col-span-6 self-center sm:self-auto", 21 | large: "md:grid-cols-2 grid-cols-2 gap-x-16 gap-y-4 col-span-6 self-center", 22 | xl: "grid-cols-2 gap-x-20 gap-y-6 col-span-12 self-stretch", 23 | }; 24 | 25 | // Combine the common classes with the size-specific classes 26 | const gridClassName = `${commonClasses} ${sizeClasses[size]} ${className}`; 27 | 28 | return ( 29 |
30 | 36 | 42 | 48 | 54 |
55 | ); 56 | }; 57 | 58 | export default PerformanceResultsGrid; 59 | -------------------------------------------------------------------------------- /src/lib/config.ts: -------------------------------------------------------------------------------- 1 | export const LOCALSCORE_VERSION = "0.9.3"; 2 | export const OFFICIAL_MODELS = [ 3 | { 4 | name: "Llama 3.2 1B Instruct", 5 | shortName: "LLama 3.2", 6 | label: "1B", 7 | humanLabel: "Tiny", 8 | quant: "Q4_K - Medium", 9 | vram: "2GB", 10 | params: "1B", 11 | hfName: "bartowski/Llama-3.2-1B-Instruct-GGUF", 12 | hfDownload: 13 | "https://huggingface.co/bartowski/Llama-3.2-1B-Instruct-GGUF/resolve/main/Llama-3.2-1B-Instruct-Q4_K_M.gguf", 14 | hfFilename: "Llama-3.2-1B-Instruct-Q4_K_M.gguf", 15 | }, 16 | { 17 | name: "Meta Llama 3.1 8B Instruct", 18 | shortName: "LLama 3.1", 19 | label: "8B", 20 | humanLabel: "Small", 21 | quant: "Q4_K - Medium", 22 | vram: "6GB", 23 | params: "8B", 24 | hfName: "bartowski/Meta-Llama-3.1-8B-Instruct-GGUF", 25 | hfDownload: 26 | "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", 27 | hfFilename: "Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf", 28 | }, 29 | { 30 | name: "Qwen2.5 14B Instruct", 31 | shortName: "Qwen 2.5", 32 | label: "14B", 33 | humanLabel: "Medium", 34 | quant: "Q4_K - Medium", 35 | vram: "10GB", 36 | params: "14B", 37 | hfName: "bartowski/Qwen2.5-14B-Instruct-GGUF", 38 | hfDownload: 39 | "https://huggingface.co/bartowski/Qwen2.5-14B-Instruct-GGUF/resolve/main/Qwen2.5-14B-Instruct-Q4_K_M.gguf", 40 | hfFilename: "Qwen2.5-14B-Instruct-Q4_K_M.gguf", 41 | }, 42 | ]; 43 | 44 | export const MODEL_MAP = OFFICIAL_MODELS.reduce( 45 | (map, model) => { 46 | map[model.label] = { name: model.name, quant: model.quant }; 47 | return map; 48 | }, 49 | {} as Record, 50 | ); 51 | 52 | export const NUM_DEFAULT_GRAPH_RESULTS = 5; 53 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Meta from "@/components/layout/Meta"; 2 | import Separator from "@/components/ui/Separator"; 3 | import { 4 | getModelVariants, 5 | getPerformanceScores, 6 | getTopAcceleratorsByModelVariants, 7 | } from "@/db/queries"; 8 | import { OFFICIAL_MODELS } from "@/lib/config"; 9 | import { PerformanceScore } from "@/lib/types"; 10 | import HomepageLeaderboard from "@/components/leaderboard/HomepageLeaderboard"; 11 | // import Link from "nextlink"; 12 | 13 | export default function Home({ results }: { results: PerformanceScore[] }) { 14 | return ( 15 | <> 16 | 17 |
18 |
19 |

20 | LocalScore is an open benchmark which helps you understand 21 | how well your computer can handle local AI tasks.{" "} 22 |

23 | {/* 24 | Learn more 25 | */} 26 |
27 | 28 |
29 | 30 | 31 | ); 32 | } 33 | 34 | export async function getStaticProps() { 35 | const models = OFFICIAL_MODELS.map((model) => ({ 36 | name: model.name, 37 | quant: model.quant, 38 | })); 39 | 40 | const modelVariants = await getModelVariants(models); 41 | const modelVariantIds = modelVariants.map((mv) => mv.variantId); 42 | const acceleratorIds = await getTopAcceleratorsByModelVariants({ 43 | modelVariantIds, 44 | numResults: 20, 45 | }); 46 | 47 | const results = await getPerformanceScores(acceleratorIds, modelVariantIds); 48 | 49 | return { 50 | props: { 51 | results, 52 | }, 53 | // Revalidate every minute 54 | revalidate: 60, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/components/charts/AcceleratorMetricsChart.tsx: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey, PerformanceScore } from "@/lib/types"; 2 | import { useMemo } from "react"; 3 | import MetricsBarChart, { ChartDataItem } from "./MetricsBarChart"; 4 | import { getColor } from "@/lib/utils"; 5 | 6 | function AcceleratorMetricsChart({ 7 | data, 8 | metricKey, 9 | acceleratorName, 10 | sortDirection = "desc", 11 | xAxisLabel = "", 12 | }: { 13 | data: PerformanceScore[]; 14 | metricKey: PerformanceMetricKey; 15 | acceleratorName: string; 16 | sortDirection?: "asc" | "desc"; 17 | xAxisLabel?: string; 18 | }) { 19 | const chartData = useMemo(() => { 20 | return data 21 | .map((modelData) => { 22 | const result = modelData.results.find( 23 | (r) => r.accelerator.name === acceleratorName 24 | ); 25 | if (result) { 26 | return { 27 | name: `${modelData.model.name} (${modelData.model.quant})`, 28 | value: result[metricKey], 29 | color: "", // Will be set after sorting 30 | }; 31 | } 32 | return null; 33 | }) 34 | .filter((item): item is ChartDataItem => item !== null) 35 | .sort((a, b) => { 36 | return sortDirection === "desc" ? b.value - a.value : a.value - b.value; 37 | }) 38 | .map((item, index) => ({ 39 | ...item, 40 | color: getColor(index, 10), 41 | })); 42 | }, [data, acceleratorName, metricKey, sortDirection]); 43 | 44 | return ( 45 | 55 | ); 56 | } 57 | 58 | export default AcceleratorMetricsChart; 59 | -------------------------------------------------------------------------------- /src/components/display/SystemInfo.tsx: -------------------------------------------------------------------------------- 1 | import { System } from "@/lib/types"; 2 | 3 | const SystemInfo: React.FC<{ systemInfo: System; extended?: boolean }> = ({ 4 | systemInfo, 5 | extended, 6 | }) => { 7 | return ( 8 |
9 |
10 |
CPU
11 |
{systemInfo.cpu_name}
12 |
13 |
14 |
RAM
15 |
{systemInfo.ram_gb}GB
16 |
17 |
18 |
OS
19 |
{systemInfo.kernel_type}
20 |
21 | {extended && ( 22 | <> 23 |
24 |
Kernel Release
25 |
{systemInfo.kernel_release}
26 |
27 |
28 |
Architecture
29 |
{systemInfo.cpu_arch}
30 |
31 | 32 |
33 |
Version
34 |
38 | {systemInfo.system_version} 39 |
40 |
41 | 42 | )} 43 |
44 | ); 45 | }; 46 | 47 | export default SystemInfo; 48 | -------------------------------------------------------------------------------- /src/pages/download/ModelTab.tsx: -------------------------------------------------------------------------------- 1 | import Button from "@/components/ui/Button"; 2 | import { TabContent } from "@/components/ui/Tab"; 3 | import React from "react"; 4 | import TabStepLabel from "./TabStepLabel"; 5 | import CodeBlock from "@/components/ui/CodeBlock"; 6 | import TabStep from "./TabStep"; 7 | import OperatingSystemSelector from "./OperatingSystemSelector"; 8 | import { useDownloadStore } from "@/lib/hooks/useDownload"; 9 | import { LOCALSCORE_VERSION } from "@/lib/config"; 10 | 11 | const ModelTab = () => { 12 | const { operatingSystem } = useDownloadStore(); 13 | 14 | const isWindows = operatingSystem === "Windows"; 15 | 16 | return ( 17 | 18 | 19 | What OS are you running? 20 | 21 | 22 | 23 | 33 | 34 | 35 | 36 | 37 | {isWindows ? "Open cmd.exe and run:" : "Open your terminal and run:"} 38 | 39 | {isWindows ? ( 40 | 41 | {`localscore-${LOCALSCORE_VERSION}.exe -m path\\to\\model.gguf`} 42 | 43 | ) : ( 44 | 45 | {`chmod +x localscore-${LOCALSCORE_VERSION} 46 | ./localscore-${LOCALSCORE_VERSION} -m path/to/model.gguf`} 47 | 48 | )} 49 | 50 | 51 | ); 52 | }; 53 | 54 | export default ModelTab; 55 | -------------------------------------------------------------------------------- /src/components/cards/compare/CompareCard.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import MetricSelector from "@/components/display/MetricSelector"; 3 | import { PerformanceMetricKey } from "@/lib/types"; 4 | import { 5 | CompareCardContainer, 6 | CompareCardSection, 7 | CompareChartContainer, 8 | CompareSectionTitle, 9 | } from "./CompareCardComponents"; 10 | 11 | interface CompareCardProps { 12 | headerText: string; 13 | itemCount: number; 14 | itemsLabel: string; 15 | selectorTitle: string; 16 | selectedKey: PerformanceMetricKey; 17 | setSelectedKey: (key: PerformanceMetricKey) => void; 18 | titleContent: ReactNode; 19 | selectorComponent: ReactNode; 20 | chartComponent: ReactNode; 21 | } 22 | 23 | function CompareCard({ 24 | headerText, 25 | itemCount, 26 | itemsLabel, 27 | selectorTitle, 28 | selectedKey, 29 | setSelectedKey, 30 | titleContent, 31 | selectorComponent, 32 | chartComponent, 33 | }: CompareCardProps) { 34 | return ( 35 | 39 | {itemCount} {itemsLabel} tested 40 |

41 | } 42 | > 43 | 44 | 48 | {selectorComponent} 49 | 50 | 51 |
52 | 53 | {titleContent} 54 |
55 | 59 |
60 |
61 | 62 | {chartComponent} 63 |
64 |
65 | ); 66 | } 67 | 68 | export default CompareCard; 69 | -------------------------------------------------------------------------------- /src/components/leaderboard/HomepageLeaderboard.tsx: -------------------------------------------------------------------------------- 1 | import { AcceleratorType, PerformanceScore } from "@/lib/types"; 2 | import { useState } from "react"; 3 | import Card from "../ui/Card"; 4 | import CardHeader from "../ui/CardHeader"; 5 | import LeaderboardTable from "./LeaderboardTable"; 6 | import LeaderboardAcceleratorSelect from "./LeaderboardAcceleratorSelect"; 7 | import LeaderboardSelectedModelHeader from "./LeaderboardSelectedModelHeader"; 8 | import { OFFICIAL_MODELS } from "@/lib/config"; 9 | import { Tab, Tabs } from "../ui/Tab"; 10 | 11 | interface LeaderboardProps { 12 | data: PerformanceScore[]; 13 | } 14 | 15 | const HomepageLeaderboard = ({ data }: LeaderboardProps) => { 16 | const [filterType, setFilterType] = useState("GPU"); 17 | 18 | // const modelToUse = data[0]?.model; 19 | // const selectedModelData = data.find((d) => d.model.name === modelToUse?.name); 20 | 21 | return ( 22 |
23 | 27 | 28 | 29 | {OFFICIAL_MODELS.map((model) => { 30 | const selectedModelData = data.find( 31 | (d) => d.model.name === model.name 32 | ); 33 | 34 | return ( 35 | 39 | 40 |
41 | 42 | 43 |
44 | 45 | 49 |
50 |
51 | ); 52 | })} 53 |
54 |
55 | ); 56 | }; 57 | 58 | export default HomepageLeaderboard; 59 | -------------------------------------------------------------------------------- /src/components/display/AcceleratorInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Accelerator } from "@/lib/types"; 2 | import Link from "next/link"; 3 | import Image from "next/image"; 4 | 5 | interface AcceleratorInfoProps extends Partial { 6 | variant?: "standard" | "header"; 7 | } 8 | 9 | const AcceleratorInfo: React.FC = ({ 10 | id, 11 | name, 12 | type, 13 | memory_gb, 14 | variant = "standard", 15 | }) => { 16 | const isHeader = variant === "header"; 17 | return ( 18 |
19 | Accelerator icon 26 |
27 |
28 | {isHeader ? ( 29 |
{name}
30 | ) : ( 31 | 35 | {name} 36 | 37 | )} 38 | 39 |
44 | {type} 45 |
46 |
47 |
48 | 49 |
54 |
59 | {memory_gb} 60 |
61 |
66 | GB 67 |
68 |
69 |
70 | ); 71 | }; 72 | 73 | export default AcceleratorInfo; 74 | -------------------------------------------------------------------------------- /src/pages/model/[id]/index.tsx: -------------------------------------------------------------------------------- 1 | import AcceleratorCompareCard from "@/components/cards/compare/AcceleratorCompareCard"; 2 | import Leaderboard from "@/components/leaderboard/Leaderboard"; 3 | import ModelInfo from "@/components/display/ModelInfo"; 4 | import PageHeader from "@/components/layout/PageHeader"; 5 | import Separator from "@/components/ui/Separator"; 6 | import { 7 | getPerformanceScores, 8 | getTopAcceleratorsByModelVariants, 9 | } from "@/db/queries"; 10 | import { PerformanceScore } from "@/lib/types"; 11 | import { GetServerSideProps } from "next"; 12 | import React from "react"; 13 | import Meta from "@/components/layout/Meta"; 14 | 15 | const Index = ({ 16 | result, 17 | id, 18 | }: { 19 | result: PerformanceScore | null; 20 | id: string; 21 | }) => { 22 | if (!result) { 23 | return
Model not found
; 24 | } 25 | 26 | return ( 27 |
28 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 | ); 44 | }; 45 | 46 | export const getServerSideProps: GetServerSideProps = async (context) => { 47 | const startTime = Date.now(); 48 | 49 | const { id: idRaw } = context.query; 50 | const id = idRaw as string; 51 | 52 | // TODO use zod in types. 53 | const modelVariantIds = [parseInt(id)]; 54 | 55 | const acceleratorIds = await getTopAcceleratorsByModelVariants({ 56 | modelVariantIds, 57 | }); 58 | 59 | const results = await getPerformanceScores(acceleratorIds, modelVariantIds); 60 | 61 | const endTime = Date.now(); 62 | console.log(`/model/${id} DB fetch took ${endTime - startTime}ms`); 63 | 64 | return { 65 | props: { 66 | result: results[0] || null, 67 | id, 68 | }, 69 | }; 70 | }; 71 | 72 | export default Index; 73 | -------------------------------------------------------------------------------- /src/components/cards/compare/CompareCardComponents.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import Card from "../../ui/Card"; 3 | import Separator from "../../ui/Separator"; 4 | 5 | // TODO it might be useful to have the card component actually have these nicely. 6 | // so it applies to more than just compare cards. 7 | 8 | interface CardHeaderProps { 9 | text: string; 10 | rightContent?: ReactNode; 11 | } 12 | 13 | interface CardSectionProps { 14 | children: ReactNode; 15 | className?: string; 16 | } 17 | 18 | interface CardContainerProps { 19 | headerText: string; 20 | headerRightContent?: ReactNode; 21 | children: ReactNode; 22 | showSeparator?: boolean; 23 | } 24 | 25 | interface SectionTitleProps { 26 | title: string; 27 | className?: string; 28 | } 29 | 30 | export const CompareCardHeader: React.FC = ({ 31 | text, 32 | rightContent, 33 | }) => { 34 | return ( 35 |
36 |
{text}
37 | {rightContent} 38 |
39 | ); 40 | }; 41 | 42 | export const CompareCardSection: React.FC = ({ 43 | children, 44 | className = "", 45 | }) => { 46 | return
{children}
; 47 | }; 48 | 49 | export const CompareSectionTitle: React.FC = ({ 50 | title, 51 | className = "", 52 | }) => { 53 | return

{title}

; 54 | }; 55 | 56 | export const CompareChartContainer = ({ 57 | children, 58 | }: { 59 | children: ReactNode; 60 | }) => { 61 | return
{children}
; 62 | }; 63 | 64 | export const CompareCardContainer: React.FC = ({ 65 | headerText, 66 | headerRightContent, 67 | children, 68 | showSeparator = true, 69 | }) => { 70 | return ( 71 | 72 | 73 | 77 | {showSeparator && } 78 | {children} 79 | 80 | 81 | ); 82 | }; 83 | -------------------------------------------------------------------------------- /src/components/leaderboard/LeaderboardAcceleratorRow.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Link from "next/link"; 3 | // import Image from "next/image"; 4 | import { LeaderboardResult } from "@/lib/types"; 5 | import { formatMetricValue } from "@/lib/utils"; 6 | import { LEADERBOARD_COLUMNS } from "./constants"; 7 | import Separator from "../ui/Separator"; 8 | 9 | interface AcceleratorRowProps { 10 | result: LeaderboardResult; 11 | } 12 | 13 | const LeaderboardAcceleratorRow: React.FC = ({ 14 | result, 15 | }) => { 16 | return ( 17 | <> 18 |
19 |
20 | {/*

#{result.performance_rank}

*/} 21 | 25 | {result.accelerator.name} 26 | 27 |
28 | 29 |
30 | {/* a small icon of a computer chip{" "} */} 36 | {result.accelerator.type} / {result.accelerator.memory_gb}GB 37 |
38 |
39 | 40 | {LEADERBOARD_COLUMNS.map((column, index) => ( 41 |
45 |
46 | {column.label} 47 |
48 |
49 | {formatMetricValue(column.key, result[column.key]).formatted} 50 |
51 |
52 | {formatMetricValue(column.key, result[column.key]).suffix} 53 |
54 |
55 | ))} 56 | 57 | ); 58 | }; 59 | 60 | export default LeaderboardAcceleratorRow; 61 | -------------------------------------------------------------------------------- /src/components/leaderboard/LeaderboardTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import Separator from "../ui/Separator"; 3 | import { 4 | AcceleratorType, 5 | PerformanceMetricKey, 6 | PerformanceScore, 7 | SortDirection, 8 | } from "@/lib/types"; 9 | import LeaderboardAcceleratorRow from "./LeaderboardAcceleratorRow"; 10 | import LeaderboardHeader from "./LeaderboardHeader"; 11 | 12 | const LeaderboardTable = ({ 13 | data, 14 | filterType, 15 | }: { 16 | data: PerformanceScore | undefined; 17 | filterType: AcceleratorType; 18 | }) => { 19 | const [sortKey, setSortKey] = 20 | useState("performance_score"); 21 | const [sortDirection, setSortDirection] = useState("desc"); 22 | 23 | if (!data) return null; 24 | 25 | const filteredData = data.results.filter((result) => { 26 | if (filterType === "ALL") return true; 27 | return result.accelerator.type === filterType; 28 | }); 29 | 30 | const handleSort = (key: PerformanceMetricKey) => { 31 | if (sortKey === key) { 32 | setSortDirection(sortDirection === "asc" ? "desc" : "asc"); 33 | } else { 34 | setSortKey(key); 35 | setSortDirection("desc"); 36 | } 37 | }; 38 | 39 | const sortedData = [...filteredData].sort((a, b) => { 40 | const multiplier = sortDirection === "asc" ? 1 : -1; 41 | return (a[sortKey] - b[sortKey]) * multiplier; 42 | }); 43 | 44 | return ( 45 |
46 | 47 |
48 | 53 |
54 | 55 | 56 | 57 |
58 | {sortedData.map((result, index) => ( 59 |
63 | 64 |
65 | ))} 66 |
67 |
68 | ); 69 | }; 70 | 71 | export default LeaderboardTable; 72 | -------------------------------------------------------------------------------- /src/components/display/ModelInfo.tsx: -------------------------------------------------------------------------------- 1 | import { Model } from "@/lib/types"; 2 | import { getModelParamsString } from "@/lib/utils"; 3 | import Image from "next/image"; 4 | import Link from "next/link"; 5 | 6 | type ModelInfoProps = Model & { 7 | variant?: "standard" | "header"; 8 | }; 9 | 10 | const ModelInfo = ({ 11 | name, 12 | quant, 13 | variantId, 14 | variant = "standard", 15 | params, 16 | }: ModelInfoProps) => { 17 | const isHeader = variant === "header"; 18 | 19 | return ( 20 |
21 | model icon 28 |
29 | {variant === "standard" ? ( 30 | 34 |

{name}

35 |

{quant}

36 | 37 | ) : ( 38 |
39 |

{name}

40 |

45 | {quant} 46 |

47 |
48 | )} 49 |
50 | 51 |
56 | 61 | {getModelParamsString(params)} 62 | 63 | 68 | params 69 | 70 |
71 |
72 | ); 73 | }; 74 | 75 | export default ModelInfo; 76 | -------------------------------------------------------------------------------- /src/components/charts/ModelMetricsChart.tsx: -------------------------------------------------------------------------------- 1 | import { PerformanceMetricKey, PerformanceScore } from "@/lib/types"; 2 | import { getColor } from "@/lib/utils"; 3 | import { useMemo } from "react"; 4 | import MetricsBarChart from "./MetricsBarChart"; 5 | 6 | function ModelMetricsChart({ 7 | data, 8 | selectedModel, 9 | highlightedAccelerator, 10 | metricKey, 11 | sortDirection = "desc", 12 | xAxisLabel = "", 13 | }: { 14 | data: PerformanceScore[]; 15 | selectedModel: { name: string; quant: string }; 16 | highlightedAccelerator?: { name: string; memory: number }; 17 | metricKey: PerformanceMetricKey; 18 | sortDirection?: "asc" | "desc"; 19 | xAxisLabel?: string; 20 | }) { 21 | const chartData = useMemo(() => { 22 | const selectedModelData = data.find( 23 | (item) => 24 | item.model.name === selectedModel.name && 25 | item.model.quant === selectedModel.quant 26 | ); 27 | 28 | if (!selectedModelData) return []; 29 | 30 | return [...selectedModelData.results] 31 | .sort((a, b) => { 32 | const aValue = a[metricKey] || 0; 33 | const bValue = b[metricKey] || 0; 34 | return sortDirection === "desc" ? bValue - aValue : aValue - bValue; 35 | }) 36 | .slice(0, 10) 37 | .map((item, idx) => { 38 | const isHighlighted = 39 | highlightedAccelerator && 40 | item.accelerator.name === highlightedAccelerator.name && 41 | item.accelerator.memory_gb == highlightedAccelerator.memory; 42 | 43 | return { 44 | name: item.accelerator.name, 45 | memory: item.accelerator.memory_gb, 46 | value: item[metricKey] || 0, 47 | // color: isHighlighted ? "#582acbee" : getColor(idx, 10), 48 | color: getColor(idx, 10), 49 | isHighlighted: !!isHighlighted, 50 | }; 51 | }); 52 | }, [data, selectedModel, highlightedAccelerator, metricKey, sortDirection]); 53 | 54 | return ( 55 | 65 | ); 66 | } 67 | 68 | export default ModelMetricsChart; 69 | -------------------------------------------------------------------------------- /src/components/layout/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactNode } from "react"; 2 | import Separator from "../ui/Separator"; 3 | import Link from "next/link"; 4 | import DiscordIcon from "../icons/DiscordIcon"; 5 | import GithubIcon from "../icons/GithubIcon"; 6 | import EmailIcon from "../icons/EmailIcon"; 7 | 8 | const FooterHyperlink = ({ 9 | href, 10 | children, 11 | className = "", 12 | }: { 13 | href: string; 14 | children: ReactNode; 15 | className?: string; 16 | }) => { 17 | return ( 18 | 22 | {children} 23 | 24 | ); 25 | }; 26 | 27 | const Footer = () => { 28 | return ( 29 |
30 | 31 |
32 | 33 | 34 | {/* Discord */} 35 | 36 |

37 | 38 |
39 | 40 | Website 41 |
42 |
43 |

44 | 45 |
46 | 47 | CLI 48 |
49 |
50 |

51 | 55 | 56 | contact@localscore.ai 57 | 58 |
59 |
60 | 61 | A Mozilla Builders Project 62 | 63 |
64 |
65 | ); 66 | }; 67 | 68 | export default Footer; 69 | -------------------------------------------------------------------------------- /src/components/icons/SearchIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const SearchIcon = ({ 4 | className = "", 5 | fill = "var(--grey-400)", 6 | }: { 7 | className?: string; 8 | fill?: string; 9 | }) => { 10 | return ( 11 | 19 | 23 | 24 | ); 25 | }; 26 | 27 | export default SearchIcon; 28 | -------------------------------------------------------------------------------- /src/components/leaderboard/LeaderboardHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { LEADERBOARD_COLUMNS } from "./constants"; 3 | import { PerformanceMetricKey, SortDirection } from "@/lib/types"; 4 | import ArrowIcon from "../icons/ArrowIcon"; 5 | 6 | const HeaderItem = ({ 7 | text, 8 | sortable = false, 9 | onClick, 10 | className, 11 | sortKey, 12 | currentSortKey, 13 | sortDirection, 14 | }: { 15 | text: string; 16 | sortable?: boolean; 17 | onClick?: () => void; 18 | className?: string; 19 | sortKey?: PerformanceMetricKey; 20 | currentSortKey?: PerformanceMetricKey; 21 | sortDirection?: SortDirection; 22 | }) => { 23 | const handleClick = (e: React.MouseEvent) => { 24 | e.preventDefault(); 25 | onClick?.(); 26 | }; 27 | 28 | const showArrow = sortable && sortKey === currentSortKey; 29 | 30 | return ( 31 |
e.preventDefault()} 37 | > 38 |
39 | {text} 40 | {showArrow && ( 41 | 42 | 47 | 48 | )} 49 |
50 |
51 | ); 52 | }; 53 | 54 | const LeaderboardHeader = ({ 55 | onSort, 56 | currentSortKey, 57 | sortDirection, 58 | }: { 59 | onSort: (key: PerformanceMetricKey) => void; 60 | currentSortKey: PerformanceMetricKey; 61 | sortDirection: SortDirection; 62 | }) => { 63 | return ( 64 | <> 65 | 66 | {LEADERBOARD_COLUMNS.map((column, index) => ( 67 | onSort(column.key)} 73 | sortKey={column.key} 74 | currentSortKey={currentSortKey} 75 | sortDirection={sortDirection} 76 | /> 77 | ))} 78 | 79 | ); 80 | }; 81 | 82 | export default LeaderboardHeader; 83 | -------------------------------------------------------------------------------- /src/components/cards/LatestRunCard.tsx: -------------------------------------------------------------------------------- 1 | import { Run } from "@/lib/types"; 2 | import Card from "../ui/Card"; 3 | import Link from "next/link"; 4 | import dayjs from "dayjs"; 5 | import timezone from "dayjs/plugin/timezone"; 6 | import utc from "dayjs/plugin/utc"; 7 | import Separator from "../ui/Separator"; 8 | import AcceleratorInfo from "../display/AcceleratorInfo"; 9 | import ModelInfo from "../display/ModelInfo"; 10 | import SystemInfo from "../display/SystemInfo"; 11 | import PerformanceResultsGrid from "../display/PerformanceResultsGrid"; 12 | 13 | dayjs.extend(timezone); 14 | dayjs.extend(utc); 15 | 16 | const LatestRunHeader = ({ run }: { run: Run }) => ( 17 |
18 |
19 | 23 | Test #{run.id} 24 | 25 |

26 | {dayjs.utc(run.created_at).local().format("MM/DD/YYYY - h:mm A")} 27 |

28 |
29 |
30 | ); 31 | 32 | const LatestRunBody = ({ run }: { run: Run }) => ( 33 |
34 |
35 | 41 | 42 | 43 | 44 | 45 |
46 | 51 | 56 | 57 |
58 | ); 59 | 60 | const LatestRunCard: React.FC<{ run: Run }> = ({ run }) => ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | ); 69 | 70 | export default LatestRunCard; 71 | -------------------------------------------------------------------------------- /src/pages/api/search.ts: -------------------------------------------------------------------------------- 1 | import db from "@/db"; 2 | import { accelerators, models, modelVariants } from "@/db/schema"; 3 | import { Column, desc, eq, SQL, sql } from "drizzle-orm"; 4 | import type { NextApiRequest, NextApiResponse } from "next"; 5 | 6 | function ilike(column: Column, pattern: string): SQL { 7 | return sql`${column} LIKE ${pattern} COLLATE NOCASE`; 8 | } 9 | 10 | export default async function handler( 11 | req: NextApiRequest, 12 | res: NextApiResponse 13 | ) { 14 | const { q, type } = req.query; 15 | if (typeof q !== "string") { 16 | return res.status(400).json({ error: "Search query required" }); 17 | } 18 | 19 | if (!["model", "accelerator", undefined].includes(type as string)) { 20 | return res.status(400).json({ error: "Invalid search type" }); 21 | } 22 | 23 | const searchTerm = `%${q.trim()}%`.toLowerCase(); 24 | 25 | const result = await db.transaction(async (tx) => { 26 | const modelResults = 27 | type === "model" || !type 28 | ? await tx 29 | .select({ 30 | name: models.name, 31 | quant: modelVariants.quantization, 32 | variantId: modelVariants.id, 33 | id: models.id, 34 | params: models.params, 35 | }) 36 | .from(models) 37 | .leftJoin(modelVariants, eq(models.id, modelVariants.model_id)) 38 | .where(ilike(models.name, searchTerm)) 39 | .orderBy(desc(models.created_at)) 40 | .limit(10) 41 | : []; 42 | 43 | const acceleratorResults = 44 | type === "accelerator" || !type 45 | ? await tx 46 | .select({ 47 | id: accelerators.id, 48 | name: accelerators.name, 49 | memory_gb: accelerators.memory_gb, 50 | type: accelerators.type, 51 | manufacturer: accelerators.manufacturer, 52 | created_at: accelerators.created_at, 53 | }) 54 | .from(accelerators) 55 | .where(ilike(accelerators.name, searchTerm)) 56 | .orderBy(desc(accelerators.created_at)) 57 | .limit(10) 58 | : []; 59 | 60 | return { 61 | models: modelResults, 62 | accelerators: acceleratorResults, 63 | }; 64 | }); 65 | 66 | return res 67 | .status(200) 68 | .json( 69 | type === "model" 70 | ? { models: result.models } 71 | : type === "accelerator" 72 | ? { accelerators: result.accelerators } 73 | : result 74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /src/components/layout/Header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import Image from "next/image"; 3 | import React from "react"; 4 | import SearchBar from "./SearchBar"; 5 | import Separator from "../ui/Separator"; 6 | import { useRouter } from "next/router"; 7 | 8 | const Header = () => { 9 | return ( 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 |
21 | ); 22 | }; 23 | 24 | const Banner = () => { 25 | return ( 26 |
27 | 28 | logo 36 | 37 |
38 | ); 39 | }; 40 | 41 | const NavigationLinks = () => { 42 | const router = useRouter(); 43 | const currentPath = router.pathname; 44 | 45 | return ( 46 |
47 | 48 | Home 49 | 50 | 51 | Latest Results 52 | 53 | 54 | Download 55 | 56 | 57 | About 58 | 59 | 60 | Blog 61 | 62 |
63 | ); 64 | }; 65 | 66 | const HeaderLink = ({ 67 | href, 68 | children, 69 | currentPath, 70 | }: { 71 | href: string; 72 | children: React.ReactNode; 73 | currentPath: string; 74 | }) => { 75 | const isActive = currentPath === href; 76 | 77 | return ( 78 | 86 | {children} 87 | 88 | ); 89 | }; 90 | 91 | export default Header; 92 | -------------------------------------------------------------------------------- /src/components/ui/CodeBlock.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactNode } from "react"; 2 | 3 | interface CodeBlockProps { 4 | children: ReactNode; 5 | className?: string; 6 | } 7 | 8 | const CodeBlock: React.FC = ({ children, className }) => { 9 | const [isCopied, setIsCopied] = useState(false); 10 | 11 | // Convert children to string for copying 12 | const codeAsString = 13 | typeof children === "string" 14 | ? children 15 | : React.isValidElement(children) && children.type === "code" 16 | ? React.Children.toArray(children.props.children).join("") 17 | : React.Children.toArray(children).join(""); 18 | 19 | const handleCopy = async () => { 20 | try { 21 | await navigator.clipboard.writeText(codeAsString); 22 | setIsCopied(true); 23 | setTimeout(() => setIsCopied(false), 2000); 24 | } catch (err) { 25 | console.error("Failed to copy text: ", err); 26 | } 27 | }; 28 | 29 | return ( 30 |
33 |
{children}
34 | 70 |
71 | ); 72 | }; 73 | 74 | export default CodeBlock; 75 | -------------------------------------------------------------------------------- /src/db/schema.sql: -------------------------------------------------------------------------------- 1 | -- Core tables 2 | CREATE TABLE accelerators ( 3 | id UUID PRIMARY KEY, 4 | name VARCHAR(255) NOT NULL, 5 | type VARCHAR(16) NOT NULL, -- intended to be cpu, gpu, tpu, etc. 6 | memory_gb DECIMAL(10,2), 7 | manufacturer VARCHAR(255), -- apple, nvidia, intel, amd, etc. 8 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 9 | ); 10 | 11 | CREATE TABLE models ( 12 | id UUID PRIMARY KEY, 13 | name VARCHAR(255) NOT NULL, 14 | -- TODO what else can we get from llama.cpp easily 15 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 16 | ); 17 | 18 | CREATE TABLE model_variants ( 19 | id UUID PRIMARY KEY, 20 | model_id UUID REFERENCES models(id), 21 | -- variant_name VARCHAR(255), -- TODO remove? 22 | quantization VARCHAR(50), 23 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 24 | ); 25 | 26 | CREATE TABLE runtimes ( 27 | id UUID PRIMARY KEY, 28 | name VARCHAR(255) NOT NULL, 29 | version VARCHAR(255), 30 | commit_hash VARCHAR(255), 31 | release_date DATE, 32 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 33 | ); 34 | -- TODO runtime types? Vision, Language, etc? 35 | 36 | -- System information for benchmark runs 37 | CREATE TABLE benchmark_systems ( 38 | id UUID PRIMARY KEY, 39 | cpu_name VARCHAR(255), 40 | cpu_architecture VARCHAR(255), 41 | ram_gb INTEGER, 42 | kernel_type VARCHAR(255), 43 | kernel_release VARCHAR(255), 44 | system_version VARCHAR(255), 45 | system_architecture VARCHAR(255), 46 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 47 | ); 48 | 49 | -- Main benchmark results table 50 | CREATE TABLE benchmark_runs ( 51 | id UUID PRIMARY KEY, 52 | system_id UUID REFERENCES benchmark_systems(id), 53 | accelerator_id UUID REFERENCES accelerators(id), 54 | model_variant_id UUID REFERENCES model_variants(id), 55 | runtime_id UUID REFERENCES runtimes(id), 56 | run_date TIMESTAMP WITH TIME ZONE, 57 | -- driver_version VARCHAR(255), 58 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 59 | ); 60 | 61 | -- Detailed test results 62 | CREATE TABLE test_results ( 63 | id UUID PRIMARY KEY, 64 | benchmark_run_id UUID REFERENCES benchmark_runs(id), 65 | test_name VARCHAR(255), -- e.g., 'pp16', 'tg32', 'pp1024+tg256' 66 | prompt_length INTEGER, 67 | generation_length INTEGER, 68 | avg_time_ms DECIMAL(15,2), 69 | power_watts DECIMAL(10,2), 70 | tokens_processed INTEGER, 71 | tokens_per_second DECIMAL(10,2), 72 | tokens_per_second_per_watt DECIMAL(10,4), 73 | context_window_size INTEGER, 74 | vram_used_mb DECIMAL(10,2), 75 | time_to_first_token_ms DECIMAL(10,2), 76 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP 77 | ); -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import chroma from "chroma-js"; 2 | import { PerformanceMetricKey } from "./types"; 3 | import { z } from "zod"; 4 | 5 | export const capitalize = (str: string) => { 6 | // Check if the input is a string and not empty 7 | if (typeof str !== "string" || str.length === 0) { 8 | return str; 9 | } 10 | 11 | // Capitalize the first letter and concatenate with the rest of the string 12 | return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); 13 | }; 14 | 15 | const scale = chroma 16 | .cubehelix() 17 | .start(280) 18 | .rotations(-0.5) 19 | .gamma(0.8) 20 | .lightness([0.3, 0.8]) 21 | .scale(); 22 | 23 | export const getColor = (index: number, max: number = 10) => { 24 | return scale(index / max).hex(); 25 | }; 26 | 27 | export function formatMetricValue( 28 | key: PerformanceMetricKey, 29 | value: number 30 | ): { formatted: string; suffix: string | null; simple: string } { 31 | if (!value) return { formatted: "N/A", suffix: null, simple: "N/A" }; 32 | switch (key) { 33 | case "avg_prompt_tps": 34 | return { 35 | formatted: value.toFixed(), 36 | suffix: "tokens/s", 37 | simple: value.toFixed(), 38 | }; 39 | 40 | case "avg_gen_tps": 41 | return { 42 | formatted: value > 100 ? value.toFixed() : value.toFixed(1), 43 | suffix: "tokens/s", 44 | simple: value > 100 ? value.toFixed() : value.toFixed(1), 45 | }; 46 | 47 | case "avg_ttft": 48 | return { 49 | formatted: value >= 1000 ? (value / 1000).toFixed(2) : value.toFixed(), 50 | suffix: value >= 1000 ? "sec" : "ms", 51 | simple: value.toFixed(), 52 | }; 53 | 54 | case "performance_score": 55 | return { 56 | formatted: value.toFixed(), 57 | suffix: null, 58 | simple: value.toFixed(), 59 | }; 60 | 61 | default: 62 | throw new Error(`Unsupported column key: ${key}`); 63 | } 64 | } 65 | 66 | export const getModelParamsString = (params: number): string => { 67 | if (params >= 1e12) { 68 | return (params / 1e12).toFixed(1) + "T"; 69 | } else if (params >= 1e9) { 70 | return (params / 1e9).toFixed(1) + "B"; 71 | } else { 72 | return (params / 1e6).toFixed() + "M"; 73 | } 74 | }; 75 | 76 | export const stringOrDateToString = z 77 | .union([z.string(), z.date(), z.null()]) 78 | .transform((val) => { 79 | if (!val) return ""; 80 | if (val instanceof Date) return val.toISOString(); 81 | return String(val); 82 | }); 83 | 84 | export const stringOrDateToDate = z 85 | .union([z.string(), z.date(), z.null()]) 86 | .transform((val) => { 87 | if (!val) return new Date(); 88 | if (val instanceof Date) return val; 89 | return new Date(val); 90 | }); 91 | 92 | export const numberOrStringToNumber = z 93 | .union([z.string(), z.number(), z.null()]) 94 | .transform((val) => (val ? Number(val) : 0)); 95 | -------------------------------------------------------------------------------- /src/pages/latest.tsx: -------------------------------------------------------------------------------- 1 | import PageHeader from "@/components/layout/PageHeader"; 2 | import { getBenchmarkResults } from "@/db/queries"; 3 | import { Run } from "@/lib/types"; 4 | import { GetServerSideProps } from "next"; 5 | import { useRouter } from "next/router"; 6 | import React from "react"; 7 | import LatestRunCard from "@/components/cards/LatestRunCard"; 8 | import Button from "@/components/ui/Button"; 9 | import Meta from "@/components/layout/Meta"; 10 | 11 | const PaginationControls = () => { 12 | const router = useRouter(); 13 | const { offset } = router.query; 14 | const currentOffset = Number(offset) || 0; 15 | 16 | const handleNext = () => { 17 | router.push({ 18 | pathname: router.pathname, 19 | query: { ...router.query, offset: currentOffset + 10 }, 20 | }); 21 | }; 22 | 23 | const handlePrevious = () => { 24 | router.push({ 25 | pathname: router.pathname, 26 | query: { ...router.query, offset: currentOffset - 10 }, 27 | }); 28 | }; 29 | 30 | return ( 31 |
32 | {currentOffset > 0 && ( 33 | 39 | )} 40 | 46 |
47 | ); 48 | }; 49 | 50 | const Latest = ({ results }: { results: Run[] }) => { 51 | return ( 52 | <> 53 | 57 | 58 |
59 | Latest LocalScore Results 60 |
61 | {results.map((run) => ( 62 | 63 | ))} 64 |
65 | 66 | 67 |
68 | 69 | ); 70 | }; 71 | 72 | export const getServerSideProps: GetServerSideProps = async (context) => { 73 | context.res.setHeader("Cache-Control", "public, s-maxage=5"); 74 | 75 | const startTime = Date.now(); 76 | const { offset } = context.query; 77 | const offsetValue = offset ? parseInt(offset as string) : 0; 78 | 79 | const results = await getBenchmarkResults({ 80 | sortDirection: "desc", 81 | limit: 10, 82 | offset: offsetValue, 83 | }); 84 | 85 | const endTime = Date.now(); 86 | console.log(`/latest DB fetch took ${endTime - startTime}ms`); 87 | 88 | return { 89 | props: { 90 | results, 91 | }, 92 | }; 93 | }; 94 | 95 | export default Latest; 96 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /src/components/cards/compare/AcceleratorCompareCard.tsx: -------------------------------------------------------------------------------- 1 | // AcceleratorCompareCard.tsx 2 | import ModelMetricsChart from "@/components/charts/ModelMetricsChart"; 3 | import { NUM_DEFAULT_GRAPH_RESULTS } from "@/lib/config"; 4 | import { 5 | Accelerator, 6 | PerformanceMetricKey, 7 | PerformanceScore, 8 | UniqueAccelerator, 9 | } from "@/lib/types"; 10 | import React, { useState } from "react"; 11 | import { useInitialCompareSelection } from "./useInitialCompareSelection"; 12 | import { MetricSortDirection } from "@/lib/constants"; 13 | import CompareCard from "./CompareCard"; 14 | import AcceleratorSelect from "./AcceleratorSelect"; 15 | 16 | const AcceleratorCompareCard = ({ 17 | result, 18 | }: { 19 | result: PerformanceScore | null; 20 | }) => { 21 | const [selectedKey, setSelectedKey] = 22 | useState("avg_gen_tps"); 23 | 24 | const { 25 | selectedItems: selectedAccelerators, 26 | setSelectedItems: setSelectedAccelerators, 27 | } = useInitialCompareSelection< 28 | { accelerator: Accelerator }, 29 | UniqueAccelerator 30 | >({ 31 | allItems: result?.results ?? [], 32 | officialItems: [], 33 | defaultCount: NUM_DEFAULT_GRAPH_RESULTS, 34 | itemMatchFn: (item, officialAcc) => 35 | officialAcc.name === item.accelerator.name && 36 | officialAcc.memory === item.accelerator.memory_gb, 37 | mapFn: (item) => ({ 38 | name: item.accelerator.name, 39 | memory: item.accelerator.memory_gb, 40 | }), 41 | }); 42 | 43 | if (!result) { 44 | return
Model not found
; 45 | } 46 | 47 | const selectedResults = { 48 | ...result, 49 | results: selectedAccelerators.length 50 | ? result.results.filter((r) => 51 | selectedAccelerators.some( 52 | (acc) => 53 | acc.name === r.accelerator.name && 54 | acc.memory === r.accelerator.memory_gb 55 | ) 56 | ) 57 | : result.results.slice(0, NUM_DEFAULT_GRAPH_RESULTS), 58 | }; 59 | 60 | const model = result.model; 61 | 62 | return ( 63 | 72 | {model.name} - {model.quant} 73 | 74 | } 75 | selectorComponent={ 76 | r.accelerator)} 79 | onChange={(accels) => 80 | setSelectedAccelerators( 81 | accels.map((a) => ({ 82 | name: a.name, 83 | memory: a.memory_gb, 84 | })) 85 | ) 86 | } 87 | defaultValue={selectedAccelerators} 88 | /> 89 | } 90 | chartComponent={ 91 | 97 | } 98 | /> 99 | ); 100 | }; 101 | 102 | export default AcceleratorCompareCard; 103 | -------------------------------------------------------------------------------- /src/pages/download/index.tsx: -------------------------------------------------------------------------------- 1 | import Meta from "@/components/layout/Meta"; 2 | import PageHeader from "@/components/layout/PageHeader"; 3 | import { Tabs, Tab } from "@/components/ui/Tab"; 4 | import { OperatingSystem } from "@/lib/types"; 5 | import { GetServerSideProps } from "next"; 6 | import React, { useEffect } from "react"; 7 | import OfficialTab from "./OfficialTab"; 8 | import ModelTab from "./ModelTab"; 9 | import { useDownloadStore } from "../../lib/hooks/useDownload"; 10 | import Hyperlink from "@/components/ui/Hyperlink"; 11 | 12 | const Download = ({ os }: { os: OperatingSystem }) => { 13 | const { setOperatingSystem } = useDownloadStore(); 14 | 15 | useEffect(() => { 16 | setOperatingSystem(os); 17 | }, [os, setOperatingSystem]); 18 | 19 | return ( 20 | <> 21 | 25 | Download LocalScore 26 |

27 | There are two ways to run LocalScore. The easiest way to get started is 28 | to download one of the Official Models. If you have .gguf models already 29 | you run LocalScore with them. 30 |

31 | 32 | Run With 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 |

45 | Having issues with the CLI client? Check out the{" "} 46 | 47 | troubleshooting guide. 48 | {" "} 49 |

50 |

51 | For further documentation on the LocalScore CLI, check out the{" "} 52 | 53 | README 54 | 55 |

56 |
57 | 58 |
59 |
60 | Need help? Check out this video 61 |
62 |
63 | 73 |
74 |
75 | 76 | ); 77 | }; 78 | 79 | export const getServerSideProps: GetServerSideProps = async (context) => { 80 | // Get user-agent from request headers 81 | const userAgent = context.req.headers["user-agent"] || ""; 82 | 83 | // Detect OS from user agent 84 | let detectedOS = "MacOS/Linux"; 85 | 86 | if (userAgent.indexOf("Win") !== -1) detectedOS = "Windows"; 87 | 88 | return { 89 | props: { 90 | os: detectedOS, 91 | }, 92 | }; 93 | }; 94 | 95 | export default Download; 96 | -------------------------------------------------------------------------------- /src/components/ui/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, ReactNode, useEffect } from "react"; 2 | 3 | interface TabProps { 4 | label: string; 5 | children: ReactNode; 6 | className?: string; 7 | } 8 | 9 | export const Tab: React.FC = ({ children }) => { 10 | return <>{children}; 11 | }; 12 | 13 | export const TabContent = ({ 14 | children, 15 | className, 16 | }: { 17 | children: ReactNode; 18 | className?: string; 19 | }) => { 20 | return
{children}
; 21 | }; 22 | 23 | type TabStyle = "tab" | "invisible"; 24 | 25 | interface TabsProps { 26 | children: React.ReactElement[]; 27 | defaultTab?: number; 28 | style?: TabStyle; 29 | className?: string; 30 | labelClassName?: string; 31 | activeTabIndex?: number; 32 | onTabChange?: (index: number) => void; 33 | } 34 | 35 | export const Tabs: React.FC = ({ 36 | children, 37 | defaultTab = 0, 38 | style = "tab", 39 | className = "", 40 | labelClassName = "", 41 | activeTabIndex, 42 | onTabChange, 43 | }) => { 44 | // Internal state for when component is uncontrolled 45 | const [internalActiveTab, setInternalActiveTab] = useState(defaultTab); 46 | 47 | // Determine whether we're in controlled or uncontrolled mode 48 | const isControlled = activeTabIndex !== undefined; 49 | 50 | // The active tab is either the external one (if provided) or our internal state 51 | const activeTab = isControlled ? activeTabIndex : internalActiveTab; 52 | 53 | // Update internal state when external activeTabIndex changes 54 | useEffect(() => { 55 | if (isControlled) { 56 | setInternalActiveTab(activeTabIndex); 57 | } 58 | }, [isControlled, activeTabIndex]); 59 | 60 | // Handle tab changes, notifying parent when in controlled mode 61 | const handleTabChange = (index: number) => { 62 | if (!isControlled) { 63 | setInternalActiveTab(index); 64 | } 65 | 66 | // Always notify parent through callback if available 67 | if (onTabChange) { 68 | onTabChange(index); 69 | } 70 | }; 71 | 72 | // Styling functions 73 | const getHeaderStyles = () => { 74 | if (style === "invisible") { 75 | return "hidden"; // Hide the tab headers completely 76 | } 77 | return "flex w-full"; // For tab style 78 | }; 79 | 80 | const getTabItemStyles = (isActive: boolean) => { 81 | if (style === "invisible") { 82 | return "hidden"; // Not used but included for completeness 83 | } 84 | // For tab style 85 | return `py-5 w-full text-center font-medium transition-colors duration-200 rounded-t-2xl ${ 86 | isActive ? "bg-white" : `bg-primary-50` 87 | } ${labelClassName}`; 88 | }; 89 | 90 | const getContentStyles = () => { 91 | if (style === "invisible") { 92 | return "w-full"; // No special styling for invisible mode 93 | } 94 | return "w-full bg-white rounded-b-2xl"; // For tab style 95 | }; 96 | 97 | return ( 98 |
99 | {/* Tab Headers - hidden when style is "invisible" */} 100 | {style !== "invisible" && ( 101 |
102 | {React.Children.map(children, (child, index) => ( 103 | 110 | ))} 111 |
112 | )} 113 | 114 | {/* Content */} 115 |
116 | {React.Children.map(children, (child, index) => ( 117 |
118 | {child.props.children} 119 |
120 | ))} 121 |
122 |
123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /src/components/ui/GenericMultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import { multiSelectStyles, selectTheme } from "@/lib/selectStyles"; 2 | import Select, { 3 | GroupBase, 4 | MultiValue, 5 | MultiValueGenericProps, 6 | } from "react-select"; 7 | import MultiSelectOption from "../select/MultiSelectOption"; 8 | import MenuListWithHeader from "../select/CustomMenuList"; 9 | 10 | interface GenericItem { 11 | id?: number; 12 | variantId?: number; 13 | name: string; 14 | [key: string]: any; 15 | } 16 | 17 | interface UniqueIdentifier { 18 | name: string; 19 | [key: string]: any; 20 | } 21 | 22 | interface GenericMultiSelectProps< 23 | T extends GenericItem, 24 | U extends UniqueIdentifier 25 | > { 26 | items: T[]; 27 | onChange: (selectedItems: T[]) => void; 28 | defaultValue?: U[]; 29 | placeholder?: string; 30 | headerText: string; 31 | itemKey: keyof T; 32 | matchFn: (item: T, uniqueItem: U) => boolean; 33 | renderOptionLabel: (item: T, isFocused: boolean) => React.ReactNode; 34 | renderMultiValueLabel: (item: T) => React.ReactNode; 35 | filterFn?: (item: T, inputValue: string) => boolean; 36 | className?: string; 37 | } 38 | 39 | interface SelectOption { 40 | value: number; 41 | item: T; 42 | } 43 | 44 | function GenericMultiSelect({ 45 | items, 46 | onChange, 47 | defaultValue = [], 48 | placeholder, 49 | headerText, 50 | itemKey, 51 | matchFn, 52 | renderOptionLabel, 53 | renderMultiValueLabel, 54 | filterFn, 55 | className = "", 56 | }: GenericMultiSelectProps) { 57 | const findMatchingItem = (uniqueItem: U): T | undefined => { 58 | return items.find((item) => matchFn(item, uniqueItem)); 59 | }; 60 | 61 | const getSelectOption = (item: T): SelectOption => { 62 | return { 63 | value: item[itemKey] as number, 64 | item, 65 | }; 66 | }; 67 | 68 | const options: SelectOption[] = items.map(getSelectOption); 69 | 70 | const defaultOptions: SelectOption[] = defaultValue 71 | .map((uniqueItem) => { 72 | const matchedItem = findMatchingItem(uniqueItem); 73 | if (!matchedItem) return null; 74 | return getSelectOption(matchedItem); 75 | }) 76 | .filter((option): option is SelectOption => option !== null); 77 | 78 | const handleChange = (selectedOptions: MultiValue>) => { 79 | const selected = selectedOptions.map((option) => option.item); 80 | onChange(selected); 81 | }; 82 | 83 | const defaultFilterFn = (item: T, inputValue: string) => { 84 | return item.name.toLowerCase().includes(inputValue.toLowerCase()); 85 | }; 86 | 87 | const GenericMultiValueLabel = ( 88 | props: MultiValueGenericProps< 89 | SelectOption, 90 | true, 91 | GroupBase> 92 | > 93 | ) => { 94 | const { data } = props; 95 | return renderMultiValueLabel(data.item); 96 | }; 97 | 98 | return ( 99 |
e.stopPropagation()}> 100 | , true> 101 | isMulti 102 | options={options} 103 | defaultValue={defaultOptions} 104 | onChange={handleChange} 105 | className={className} 106 | placeholder={placeholder || `Select ${headerText.toLowerCase()}...`} 107 | classNamePrefix="select" 108 | styles={multiSelectStyles} 109 | hideSelectedOptions={false} 110 | filterOption={(option, inputValue) => { 111 | return (filterFn || defaultFilterFn)(option.data.item, inputValue); 112 | }} 113 | theme={selectTheme} 114 | components={{ 115 | Option: (props) => ( 116 | 117 | {renderOptionLabel(props.data.item, props.isFocused)} 118 | 119 | ), 120 | MultiValueLabel: GenericMultiValueLabel, 121 | MenuList: (props) => ( 122 | 123 | ), 124 | }} 125 | /> 126 |
127 | ); 128 | } 129 | 130 | export default GenericMultiSelect; 131 | -------------------------------------------------------------------------------- /src/components/informational/TestDescriptionsTable.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const testScenarios = [ 4 | { 5 | name: "pp1024+tg16", 6 | promptProcessing: 1024, 7 | textGeneration: 16, 8 | description: "Classification, sentiment analysis, keyword extraction.", // Medium prompt, tiny output 9 | }, 10 | { 11 | name: "pp4096+tg256", 12 | promptProcessing: 4096, 13 | textGeneration: 256, 14 | description: "Long document Q&A, RAG, short summary of extensive text.", // Very long prompt, short output 15 | }, 16 | { 17 | name: "pp2048+tg256", 18 | promptProcessing: 2048, 19 | textGeneration: 256, 20 | description: "Article summarization, contextual paragraph generation.", // Long prompt, short output 21 | }, 22 | { 23 | name: "pp2048+tg768", 24 | promptProcessing: 2048, 25 | textGeneration: 768, 26 | description: 27 | "Drafting detailed replies, multi-paragraph generation, content sections.", // Long prompt, medium output 28 | }, 29 | { 30 | name: "pp1024+tg1024", 31 | promptProcessing: 1024, 32 | textGeneration: 1024, 33 | description: 34 | "Balanced Q&A, content drafting, code generation based on long sample.", // Medium prompt, medium output 35 | }, 36 | { 37 | name: "pp1280+tg3072", 38 | promptProcessing: 1280, 39 | textGeneration: 3072, 40 | description: 41 | "Complex reasoning, chain-of-thought, long-form creative writing, code generation.", // Medium prompt, very long output 42 | }, 43 | { 44 | name: "pp384+tg1152", 45 | promptProcessing: 384, 46 | textGeneration: 1152, 47 | description: 48 | "Prompt expansion, explanation generation, creative writing, code generation.", // Short prompt, long output 49 | }, 50 | { 51 | name: "pp64+tg1024", 52 | promptProcessing: 64, 53 | textGeneration: 1024, 54 | description: 55 | "Short prompt creative generation (poetry/story), Q&A, code generation.", // Very short prompt, long output 56 | }, 57 | { 58 | name: "pp16+tg1536", 59 | promptProcessing: 16, 60 | textGeneration: 1536, 61 | description: "Creative text writing/storytelling, Q&A, code generation.", // Tiny prompt, very long output 62 | }, 63 | ]; 64 | 65 | const TestDescriptionsTable = () => { 66 | return ( 67 |
68 | 69 | 70 | 71 | 74 | 77 | 80 | 81 | 82 | 83 | {testScenarios.map((test, index) => ( 84 | 85 | 93 | 101 | 104 | 105 | ))} 106 | 107 |
72 | PROMPT TOKENS 73 | 75 | TEXT GENERATION 76 | 78 | SAMPLE USE CASES 79 |
86 |
87 | {test.promptProcessing} 88 | 89 | tokens 90 | 91 |
92 |
94 |
95 | {test.textGeneration} 96 | 97 | tokens 98 | 99 |
100 |
102 |
{test.description}
103 |
108 |
109 | ); 110 | }; 111 | 112 | export default TestDescriptionsTable; 113 | -------------------------------------------------------------------------------- /src/components/cards/compare/ModelCompareCard.tsx: -------------------------------------------------------------------------------- 1 | import AcceleratorMetricsChart from "@/components/charts/AcceleratorMetricsChart"; 2 | import { NUM_DEFAULT_GRAPH_RESULTS, OFFICIAL_MODELS } from "@/lib/config"; 3 | import { 4 | Accelerator, 5 | Model, 6 | PerformanceMetricKey, 7 | PerformanceScore, 8 | UniqueModel, 9 | } from "@/lib/types"; 10 | import React, { useState } from "react"; 11 | import CompareCard from "./CompareCard"; 12 | import GenericMultiSelect from "@/components/ui/GenericMultiSelect"; 13 | import ModelSelectOptionLabel from "@/components/select/ModelSelectOptionLabel"; 14 | import { useInitialCompareSelection } from "./useInitialCompareSelection"; 15 | import { MetricSortDirection } from "@/lib/constants"; 16 | 17 | interface ModelSelectProps { 18 | models: Model[]; 19 | onChange: (selectedModels: Model[]) => void; 20 | defaultValue?: UniqueModel[]; 21 | } 22 | 23 | const ModelSelect: React.FC = ({ 24 | models, 25 | onChange, 26 | defaultValue = [], 27 | }) => { 28 | return ( 29 | 37 | model.name === uniqueModel.name && model.quant === uniqueModel.quant 38 | } 39 | renderOptionLabel={(model, isFocused) => ( 40 | 41 | )} 42 | renderMultiValueLabel={(model) => ( 43 |
44 |

{model.name}

45 |

{model.quant}

46 |
47 | )} 48 | /> 49 | ); 50 | }; 51 | 52 | const ModelCompareCard = ({ 53 | results, 54 | accelerator, 55 | }: { 56 | results: PerformanceScore[] | null; 57 | accelerator: Accelerator; 58 | }) => { 59 | const [selectedKey, setSelectedKey] = 60 | useState("avg_gen_tps"); 61 | 62 | const { selectedItems: selectedModels, setSelectedItems: setSelectedModels } = 63 | useInitialCompareSelection({ 64 | allItems: results ?? [], 65 | officialItems: OFFICIAL_MODELS, 66 | defaultCount: NUM_DEFAULT_GRAPH_RESULTS, 67 | itemMatchFn: (result, officialModel) => 68 | officialModel.name === result.model.name && 69 | officialModel.quant === result.model.quant, 70 | mapFn: (result) => ({ 71 | name: result.model.name, 72 | quant: result.model.quant, 73 | }), 74 | }); 75 | 76 | if (!results) { 77 | return
Accelerator not found
; 78 | } 79 | const models: Model[] = results.map((result) => result.model); 80 | 81 | const selectedResults = selectedModels.length 82 | ? results.filter((result) => 83 | selectedModels.some( 84 | (model) => 85 | model.name === result.model.name && 86 | model.quant === result.model.quant 87 | ) 88 | ) 89 | : results.slice(0, NUM_DEFAULT_GRAPH_RESULTS); 90 | 91 | return ( 92 | 101 | {accelerator.name} - {accelerator.memory_gb}GB 102 | 103 | } 104 | selectorComponent={ 105 | 111 | } 112 | chartComponent={ 113 | 119 | } 120 | /> 121 | ); 122 | }; 123 | 124 | export default ModelCompareCard; 125 | -------------------------------------------------------------------------------- /src/pages/download/OfficialTab.tsx: -------------------------------------------------------------------------------- 1 | import { Tab, TabContent, Tabs } from "@/components/ui/Tab"; 2 | import { LOCALSCORE_VERSION, OFFICIAL_MODELS } from "@/lib/config"; 3 | import React from "react"; 4 | import TabStepLabel from "./TabStepLabel"; 5 | import CodeBlock from "@/components/ui/CodeBlock"; 6 | import { useDownloadStore } from "../../lib/hooks/useDownload"; 7 | import TabStep from "./TabStep"; 8 | import OperatingSystemSelector from "./OperatingSystemSelector"; 9 | import Hyperlink from "@/components/ui/Hyperlink"; 10 | 11 | const ModelSelector = () => { 12 | const { setSelectedModelIndex, selectedModelIndex } = useDownloadStore(); 13 | 14 | return ( 15 |
16 | {OFFICIAL_MODELS.map((m, i) => ( 17 | 31 | ))} 32 |
33 | ); 34 | }; 35 | 36 | const OfficialTab = () => { 37 | const { selectedModel, operatingSystem, setOperatingSystem } = 38 | useDownloadStore(); 39 | 40 | const isWindows = operatingSystem === "Windows"; 41 | const activeOSTab = isWindows ? 0 : 1; 42 | 43 | const selectedModelFilename = `localscore-${selectedModel.humanLabel.toLowerCase()}`; 44 | 45 | return ( 46 | 47 | 48 | What OS are you running? 49 | 50 | 51 | 52 | Select the benchmark you want to run: 53 | 54 | 55 | {isWindows && ( 56 | <> 57 | 58 |
59 | 1. 60 | 65 | Download LocalScore 66 | 67 |
68 |
69 | 70 |
71 | 2. 72 | 77 | Download {selectedModel.hfName} 78 | 79 |
80 |
81 | 82 | )} 83 | 84 | {!isWindows && Open your terminal and run:} 85 | 89 | i == 0 90 | ? setOperatingSystem("Windows") 91 | : setOperatingSystem("MacOS/Linux") 92 | } 93 | > 94 | 95 |
96 | Open cmd.exe and run: 97 |
98 |
99 | 3. 100 | 101 | {`localscore-${LOCALSCORE_VERSION}.exe -m ${selectedModel.hfFilename}`} 102 | 103 |
104 |
105 | 106 | 107 | {`curl -OL https://localscore.ai/download/${selectedModelFilename} 108 | chmod +x ${selectedModelFilename} 109 | ./${selectedModelFilename}`} 110 | 111 | 112 |
113 |
114 |
115 | ); 116 | }; 117 | 118 | export default OfficialTab; 119 | -------------------------------------------------------------------------------- /migrations/0000_dear_veda.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `accelerator_model_performance_scores` ( 2 | `accelerator_id` integer NOT NULL, 3 | `accelerator_name` text NOT NULL, 4 | `accelerator_type` text NOT NULL, 5 | `accelerator_memory_gb` real NOT NULL, 6 | `model_id` integer NOT NULL, 7 | `model_name` text NOT NULL, 8 | `model_variant_id` integer NOT NULL, 9 | `model_variant_quant` text NOT NULL, 10 | `avg_prompt_tps` real, 11 | `avg_gen_tps` real, 12 | `avg_ttft` real, 13 | `performance_score` real, 14 | FOREIGN KEY (`accelerator_id`) REFERENCES `accelerators`(`id`) ON UPDATE no action ON DELETE no action, 15 | FOREIGN KEY (`model_id`) REFERENCES `models`(`id`) ON UPDATE no action ON DELETE no action, 16 | FOREIGN KEY (`model_variant_id`) REFERENCES `model_variants`(`id`) ON UPDATE no action ON DELETE no action 17 | ); 18 | --> statement-breakpoint 19 | CREATE UNIQUE INDEX `accelerator_model_performance_scores_unique_idx` ON `accelerator_model_performance_scores` (`accelerator_id`,`model_id`,`model_variant_id`);--> statement-breakpoint 20 | CREATE TABLE `accelerators` ( 21 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 22 | `name` text NOT NULL, 23 | `type` text NOT NULL, 24 | `memory_gb` real NOT NULL, 25 | `manufacturer` text, 26 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP) 27 | ); 28 | --> statement-breakpoint 29 | CREATE UNIQUE INDEX `accelerator_unique_idx` ON `accelerators` (`name`,`type`,`memory_gb`);--> statement-breakpoint 30 | CREATE TABLE `benchmark_runs` ( 31 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 32 | `system_id` integer, 33 | `accelerator_id` integer, 34 | `model_variant_id` integer, 35 | `runtime_id` integer, 36 | `avg_prompt_tps` real NOT NULL, 37 | `avg_gen_tps` real NOT NULL, 38 | `avg_ttft_ms` real NOT NULL, 39 | `performance_score` real NOT NULL, 40 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP), 41 | FOREIGN KEY (`system_id`) REFERENCES `benchmark_systems`(`id`) ON UPDATE no action ON DELETE no action, 42 | FOREIGN KEY (`accelerator_id`) REFERENCES `accelerators`(`id`) ON UPDATE no action ON DELETE no action, 43 | FOREIGN KEY (`model_variant_id`) REFERENCES `model_variants`(`id`) ON UPDATE no action ON DELETE no action, 44 | FOREIGN KEY (`runtime_id`) REFERENCES `runtimes`(`id`) ON UPDATE no action ON DELETE no action 45 | ); 46 | --> statement-breakpoint 47 | CREATE TABLE `benchmark_systems` ( 48 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 49 | `cpu_name` text, 50 | `cpu_arch` text, 51 | `ram_gb` real, 52 | `kernel_type` text, 53 | `kernel_release` text, 54 | `system_version` text, 55 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP) 56 | ); 57 | --> statement-breakpoint 58 | CREATE UNIQUE INDEX `system_unique_idx` ON `benchmark_systems` (`cpu_name`,`cpu_arch`,`ram_gb`,`kernel_type`,`kernel_release`,`system_version`);--> statement-breakpoint 59 | CREATE TABLE `model_variants` ( 60 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 61 | `model_id` integer, 62 | `quantization` text NOT NULL, 63 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP), 64 | FOREIGN KEY (`model_id`) REFERENCES `models`(`id`) ON UPDATE no action ON DELETE no action 65 | ); 66 | --> statement-breakpoint 67 | CREATE UNIQUE INDEX `model_variant_unique_idx` ON `model_variants` (`model_id`,`quantization`);--> statement-breakpoint 68 | CREATE TABLE `models` ( 69 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 70 | `name` text NOT NULL, 71 | `params` integer NOT NULL, 72 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP) 73 | ); 74 | --> statement-breakpoint 75 | CREATE UNIQUE INDEX `model_name_idx` ON `models` (`name`);--> statement-breakpoint 76 | CREATE TABLE `runtimes` ( 77 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 78 | `name` text NOT NULL, 79 | `version` text, 80 | `commit_hash` text, 81 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP) 82 | ); 83 | --> statement-breakpoint 84 | CREATE UNIQUE INDEX `runtime_unique_idx` ON `runtimes` (`name`,`version`,`commit_hash`);--> statement-breakpoint 85 | CREATE TABLE `test_results` ( 86 | `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, 87 | `benchmark_run_id` integer, 88 | `name` text, 89 | `n_prompt` integer, 90 | `n_gen` integer, 91 | `avg_time_ms` real, 92 | `power_watts` real, 93 | `prompt_tps` real, 94 | `gen_tps` real, 95 | `prompt_tps_watt` real, 96 | `gen_tps_watt` real, 97 | `vram_used_mb` real, 98 | `ttft_ms` real, 99 | `created_at` integer DEFAULT (CURRENT_TIMESTAMP), 100 | FOREIGN KEY (`benchmark_run_id`) REFERENCES `benchmark_runs`(`id`) ON UPDATE no action ON DELETE no action 101 | ); 102 | -------------------------------------------------------------------------------- /src/db/schema.txt: -------------------------------------------------------------------------------- 1 | import { 2 | pgTable, 3 | uuid, 4 | varchar, 5 | decimal, 6 | timestamp, 7 | integer, 8 | date, 9 | uniqueIndex, 10 | } from "drizzle-orm/pg-core"; 11 | 12 | // Core tables 13 | export const accelerators = pgTable( 14 | "accelerators", 15 | { 16 | id: uuid("id").primaryKey(), 17 | name: varchar("name", { length: 255 }).notNull(), 18 | type: varchar("type", { length: 16 }).notNull(), 19 | memory_gb: decimal("memory_gb", { precision: 10, scale: 2 }).notNull(), 20 | manufacturer: varchar("manufacturer", { length: 255 }), 21 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 22 | }, 23 | (table) => ({ 24 | uniqueAccelerator: uniqueIndex("accelerator_unique_idx").on( 25 | table.name, 26 | table.type, 27 | table.memory_gb 28 | ), 29 | }) 30 | ); 31 | 32 | export const models = pgTable("models", { 33 | id: uuid("id").primaryKey(), 34 | name: varchar("name", { length: 255 }).notNull().unique(), 35 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 36 | }); 37 | 38 | export const modelVariants = pgTable( 39 | "model_variants", 40 | { 41 | id: uuid("id").primaryKey(), 42 | model_id: uuid("model_id").references(() => models.id), 43 | quantization: varchar("quantization", { length: 50 }), 44 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 45 | }, 46 | (table) => ({ 47 | // A model should only have one variant per quantization 48 | modelQuantizationIndex: uniqueIndex("model_variant_unique_idx").on( 49 | table.model_id, 50 | table.quantization 51 | ), 52 | }) 53 | ); 54 | 55 | export const runtimes = pgTable( 56 | "runtimes", 57 | { 58 | id: uuid("id").primaryKey(), 59 | name: varchar("name", { length: 255 }).notNull(), 60 | version: varchar("version", { length: 255 }), 61 | commit_hash: varchar("commit_hash", { length: 255 }), 62 | release_date: date("release_date"), 63 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 64 | }, 65 | (table) => ({ 66 | // A runtime should be unique based on name, version, and commit_hash 67 | runtimeUniqueIndex: uniqueIndex("runtime_unique_idx").on( 68 | table.name, 69 | table.version, 70 | table.commit_hash 71 | ), 72 | }) 73 | ); 74 | 75 | export const benchmarkSystems = pgTable("benchmark_systems", { 76 | id: uuid("id").primaryKey(), 77 | cpu_name: varchar("cpu_name", { length: 255 }), 78 | cpu_architecture: varchar("cpu_architecture", { length: 255 }), 79 | ram_gb: integer("ram_gb"), 80 | kernel_type: varchar("kernel_type", { length: 255 }), 81 | kernel_release: varchar("kernel_release", { length: 255 }), 82 | system_version: varchar("system_version", { length: 255 }), 83 | system_architecture: varchar("system_architecture", { length: 255 }), 84 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 85 | }); 86 | 87 | export const benchmarkRuns = pgTable("benchmark_runs", { 88 | id: uuid("id").primaryKey(), 89 | system_id: uuid("system_id").references(() => benchmarkSystems.id), 90 | accelerator_id: uuid("accelerator_id").references(() => accelerators.id), 91 | model_variant_id: uuid("model_variant_id").references(() => modelVariants.id), 92 | runtime_id: uuid("runtime_id").references(() => runtimes.id), 93 | run_date: timestamp("run_date", { withTimezone: true }), 94 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 95 | }); 96 | 97 | export const testResults = pgTable("test_results", { 98 | id: uuid("id").primaryKey(), 99 | benchmark_run_id: uuid("benchmark_run_id").references(() => benchmarkRuns.id), 100 | test_name: varchar("test_name", { length: 255 }), 101 | prompt_length: integer("prompt_length"), 102 | generation_length: integer("generation_length"), 103 | avg_time_ms: decimal("avg_time_ms", { precision: 15, scale: 2 }), 104 | power_watts: decimal("power_watts", { precision: 10, scale: 2 }), 105 | tokens_processed: integer("tokens_processed"), 106 | tokens_per_second: decimal("tokens_per_second", { precision: 10, scale: 2 }), 107 | tokens_per_second_per_watt: decimal("tokens_per_second_per_watt", { 108 | precision: 10, 109 | scale: 4, 110 | }), 111 | context_window_size: integer("context_window_size"), 112 | vram_used_mb: decimal("vram_used_mb", { precision: 10, scale: 2 }), 113 | time_to_first_token_ms: decimal("time_to_first_token_ms", { 114 | precision: 10, 115 | scale: 2, 116 | }), 117 | created_at: timestamp("created_at", { withTimezone: true }).defaultNow(), 118 | }); 119 | -------------------------------------------------------------------------------- /src/lib/types.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { numberOrStringToNumber, stringOrDateToString } from "./utils"; 3 | 4 | export type SortDirection = "asc" | "desc"; 5 | export type OperatingSystem = "Windows" | "MacOS/Linux"; 6 | 7 | // Model 8 | export const UniqueModelSchema = z.object({ 9 | name: z.string(), 10 | quant: z.string(), 11 | }); 12 | export const ModelSchema = UniqueModelSchema.extend({ 13 | id: z.number(), 14 | variantId: z.number(), 15 | params: z.number(), 16 | }); 17 | 18 | // Accelerator 19 | export const AcceleratorTypes = ["CPU", "GPU", "ALL"] as const; 20 | export const AcceleratorTypeSchema = z.enum(AcceleratorTypes); 21 | export const UniqueAcceleratorSchema = z.object({ 22 | name: z.string(), 23 | memory: z.number(), 24 | }); 25 | export const AcceleratorSchema = z.object({ 26 | name: z.string(), 27 | type: z.string(), 28 | id: z.number(), 29 | memory_gb: z.number(), 30 | manufacturer: z.string().nullable(), 31 | created_at: stringOrDateToString.nullable(), 32 | }); 33 | 34 | export const SortableResultSchema = z.object({ 35 | avg_prompt_tps: numberOrStringToNumber, 36 | avg_gen_tps: numberOrStringToNumber, 37 | avg_ttft: numberOrStringToNumber, 38 | performance_score: numberOrStringToNumber, 39 | }); 40 | 41 | export type SortableResult = z.infer; 42 | export type PerformanceMetricKey = keyof z.infer; 43 | export const sortableResultKeys: PerformanceMetricKey[] = Object.keys( 44 | SortableResultSchema.shape 45 | ) as PerformanceMetricKey[]; 46 | 47 | export const LeaderboardResultSchema = SortableResultSchema.extend({ 48 | performance_rank: numberOrStringToNumber, 49 | number_ranked: numberOrStringToNumber, 50 | accelerator: AcceleratorSchema, 51 | // model: MddodelSchema, 52 | }); 53 | 54 | export type LeaderboardResult = z.infer; 55 | 56 | export const PerformanceScoreSchema = z.object({ 57 | model: ModelSchema, 58 | results: z.array(LeaderboardResultSchema), 59 | }); 60 | 61 | export const PerformanceScoresSchema = z.array(PerformanceScoreSchema); 62 | 63 | export type PerformanceScore = z.infer; 64 | 65 | export type SearchTypes = "model" | "accelerator"; 66 | 67 | export interface SearchBarOption { 68 | value: string; 69 | group: SearchTypes; 70 | model?: Model; 71 | accelerator?: Accelerator; 72 | } 73 | 74 | export const SearchResponseSchema = z.object({ 75 | models: z.array( 76 | z.object({ 77 | variantId: z.number(), 78 | id: z.number(), 79 | name: z.string(), 80 | quant: z.string(), 81 | params: numberOrStringToNumber, 82 | }) 83 | ), 84 | accelerators: z.array( 85 | z.object({ 86 | id: z.number(), 87 | name: z.string(), 88 | memory_gb: z.number(), 89 | type: AcceleratorTypeSchema, 90 | manufacturer: z.string().nullable(), 91 | created_at: stringOrDateToString.nullable(), 92 | }) 93 | ), 94 | }); 95 | 96 | export type SearchResponse = z.infer; 97 | 98 | export const SystemSchema = z.object({ 99 | id: z.number(), 100 | cpu_name: z.string(), 101 | cpu_arch: z.string(), 102 | ram_gb: numberOrStringToNumber, 103 | kernel_type: z.string(), 104 | kernel_release: z.string(), 105 | system_version: z.string(), 106 | created_at: stringOrDateToString, 107 | }); 108 | 109 | export const RuntimeSchema = z.object({ 110 | id: z.number(), 111 | name: z.string(), 112 | version: z.string().nullable(), 113 | commit_hash: z.string().nullable(), 114 | created_at: stringOrDateToString.nullable(), 115 | }); 116 | 117 | export const RunSchema = z.object({ 118 | id: z.number(), 119 | system_id: z.number(), 120 | accelerator_id: z.number(), 121 | model_variant_id: z.number(), 122 | runtime_id: z.number(), 123 | created_at: stringOrDateToString, 124 | accelerator: z.string(), 125 | accelerator_type: z.string(), 126 | accelerator_memory_gb: z.number(), 127 | model: ModelSchema, 128 | avg_prompt_tps: numberOrStringToNumber, 129 | avg_gen_tps: numberOrStringToNumber, 130 | avg_ttft_ms: numberOrStringToNumber, 131 | performance_score: numberOrStringToNumber, 132 | system: SystemSchema, 133 | runtime: RuntimeSchema, 134 | }); 135 | 136 | export const TestResultSchema = z.object({ 137 | id: z.number(), 138 | benchmark_run_id: z.number(), 139 | name: z.string(), 140 | n_prompt: z.number(), 141 | n_gen: z.number(), 142 | avg_time_ms: z.number(), 143 | power_watts: z.number(), 144 | prompt_tps: z.number(), 145 | gen_tps: z.number(), 146 | prompt_tps_watt: z.number(), 147 | gen_tps_watt: z.number(), 148 | ttft_ms: z.number(), 149 | created_at: z.string(), 150 | }); 151 | 152 | export const RunsSchemaWithDetailedResults = RunSchema.extend({ 153 | results: z.array(TestResultSchema), 154 | }); 155 | 156 | export type Runtime = z.infer; 157 | 158 | export const RunsSchema = z.array(RunSchema); 159 | 160 | export type Model = z.infer; 161 | export type Accelerator = z.infer; 162 | export type UniqueModel = z.infer; 163 | 164 | export type UniqueAccelerator = z.infer; 165 | 166 | export type Run = z.infer; 167 | export type DetailedRun = z.infer; 168 | export type System = z.infer; 169 | 170 | export type AcceleratorType = (typeof AcceleratorTypes)[number]; 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LocalScore 2 | 3 | LocalScore is an open-source benchmarking tool and public database for measuring how fast Large Language Models (LLMs) run on your specific hardware. 4 | 5 | Check out [localscore.ai](https://localscore.ai) to explore benchmark results. 6 | 7 |

8 | 9 |
10 |
11 | LocalScore is a Mozilla Builders project. 12 |
13 |

14 | 15 | ## About 16 | 17 | LocalScore helps answer questions like: 18 | - Can my computer run an 8 billion parameter LLM? 19 | - Which GPU should I buy for my local AI setup? 20 | - How does my current hardware stack up against others? 21 | 22 | It measures three key performance metrics: 23 | - **Prompt Processing Speed**: How quickly your system processes input text (tokens per second) 24 | - **Generation Speed**: How fast your system generates new text (tokens per second) 25 | - **Time to First Token**: The latency before the response begins to appear (milliseconds) 26 | 27 | These metrics are combined into a **LocalScore**, making it easy to compare different hardware configurations. A score of 1,000 is excellent, 250 is passable, and below 100 will likely be a poor user experience or have significant tradeoffs. 28 | 29 | LocalScore leverages [Llamafile](https://github.com/Mozilla-Ocho/llamafile) to ensure portability and acceleration across different systems. 30 | 31 | ## Supported Hardware 32 | 33 | - CPUs (various architectures) 34 | - NVIDIA GPUs 35 | - AMD GPUs 36 | - Apple Silicon (M series) 37 | 38 | Currently supports single-GPU setups, which represents the most practical approach for most users running LLMs locally. 39 | 40 | ## Database Submissions 41 | 42 | LocalScore maintains a public database of benchmark results. Currently, submissions are accepted from: 43 | 44 | - The official LocalScore CLI client 45 | 46 | We welcome contributions from other clients in the future. If you're developing a client that would like to submit to the LocalScore database, please ensure it conforms to the submission specification defined in `src/pages/api/results`. Please reach out to [contact@localscore.ai](mailto:contact@localscore.ai) for the inclusion of your client. 47 | 48 | The submission API expects properly formatted benchmark data including hardware details, model information, and performance metrics. Reviewing the existing implementation will provide the best guidance on the expected format. 49 | 50 | ## Stack 51 | 52 | This is a Next.js Pages Router application. It uses SQLite (via libSQL) for the database and Drizzle ORM for database interactions. The repo ships with an example SQLite database which can be used for development and testing. 53 | 54 | ## Prerequisites 55 | 56 | - [Bun](https://bun.sh/) / Node.js 57 | 58 | ## Local Development Setup 59 | 60 | 1. Install Bun (or the Node.js runtime of your choice) if you haven't already: 61 | ```bash 62 | curl -fsSL https://bun.sh/install | bash 63 | ``` 64 | 65 | After installation, you may need to add Bun to your PATH. Follow the instructions displayed after installation. 66 | 67 | 2. Clone the repository: 68 | ```bash 69 | git clone git@github.com:cjpais/LocalScore.git 70 | cd LocalScore 71 | ``` 72 | 73 | 3. Install dependencies: 74 | ```bash 75 | bun install 76 | ``` 77 | 78 | 4. Start the development server: 79 | ```bash 80 | bun dev 81 | ``` 82 | 83 | 5. Open your browser and navigate to [http://localhost:3000](http://localhost:3000) 84 | 85 | ## Database Setup 86 | 87 | An example SQLite database is included in the repository, so there's no need to set up a database for local development. 88 | 89 | However if you wish to use a remote (libSQL) database. Configure the following .env vars. Currently Turso is used in production, but other libSQL remote databases can be used. 90 | 91 | ```bash 92 | TURSO_DATABASE_URL= 93 | TURSO_AUTH_TOKEN= 94 | ``` 95 | 96 | ## Contributing 97 | 98 | Contributions are welcome! Here's how you can help: 99 | 100 | ### Setting Up for Development 101 | 102 | 1. Fork the repository 103 | 2. Clone your fork: 104 | ```bash 105 | git clone git@github.com:cjpais/LocalScore.git 106 | ``` 107 | 3. Add the upstream repository: 108 | ```bash 109 | git remote add upstream git@github.com:cjpais/LocalScore.git 110 | ``` 111 | 4. Create a new branch for your feature: 112 | ```bash 113 | git checkout -b feature/your-feature-name 114 | ``` 115 | 5. Make a pull request when you're ready 116 | 117 | ### Contribution Guidelines 118 | 119 | - **Code Style**: Follow the existing code style and use TypeScript 120 | - **Documentation**: Update documentation for any changes you make 121 | - **Pull Requests**: Keep PRs focused on a single feature or bug fix 122 | - **Issues**: Check existing issues before opening a new one 123 | 124 | ## Future Work 125 | 126 | We are thinking of some features to add in the future: 127 | 128 | * **API Endpoints**: Add public API endpoints for querying the database. If you have ideas for what you would build and what you would want/need, please let us know. 129 | * **Multi-GPU Support**: Add support for multi-GPU setups. 130 | * **Upstreaming to llama.cpp**: If the changes are welcome, we would love to upstream the LocalScore CLI client to llama.cpp. 131 | 132 | If you have any ideas for features or improvements, please open an issue or submit a pull request. 133 | 134 | ## Feedback 135 | 136 | We would love to hear your feedback! Please open an Issue/Disccusion or reach out to [contact@localscore.ai](mailto:contact@localscore.ai) with any suggestions, questions, or comments. 137 | 138 | ## Acknowledgements 139 | 140 | LocalScore was created with support from [Mozilla Builders](https://builders.mozilla.org/) as a resource for the AI community. It builds upon the excellent work of [llama.cpp](https://github.com/ggml-org/llama.cpp) and [Llamafile](https://github.com/Mozilla-Ocho/llamafile). 141 | 142 | ## 📄 License 143 | 144 | This project is licensed under the [Apache 2.0 License](LICENSE). 145 | -------------------------------------------------------------------------------- /src/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { sql } from "drizzle-orm"; 2 | import { 3 | sqliteTable, 4 | text, 5 | real, 6 | integer, 7 | uniqueIndex, 8 | } from "drizzle-orm/sqlite-core"; 9 | 10 | export const accelerators = sqliteTable( 11 | "accelerators", 12 | { 13 | id: integer("id").primaryKey({ autoIncrement: true }), 14 | name: text("name").notNull(), 15 | type: text("type").notNull(), 16 | memory_gb: real("memory_gb").notNull(), 17 | manufacturer: text("manufacturer"), 18 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 19 | }, 20 | (table) => ({ 21 | acceleratorUniqueIdx: uniqueIndex("accelerator_unique_idx").on( 22 | table.name, 23 | table.type, 24 | table.memory_gb 25 | ), 26 | }) 27 | ); 28 | 29 | export const models = sqliteTable( 30 | "models", 31 | { 32 | id: integer("id").primaryKey({ autoIncrement: true }), 33 | name: text("name").notNull(), 34 | params: integer("params").notNull(), 35 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 36 | }, 37 | (table) => ({ 38 | nameIdx: uniqueIndex("model_name_idx").on(table.name), 39 | }) 40 | ); 41 | 42 | export const modelVariants = sqliteTable( 43 | "model_variants", 44 | { 45 | id: integer("id").primaryKey({ autoIncrement: true }), 46 | model_id: integer("model_id").references(() => models.id), 47 | quantization: text("quantization").notNull(), 48 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 49 | }, 50 | (table) => ({ 51 | modelVariantUniqueIdx: uniqueIndex("model_variant_unique_idx").on( 52 | table.model_id, 53 | table.quantization 54 | ), 55 | }) 56 | ); 57 | 58 | export const runtimes = sqliteTable( 59 | "runtimes", 60 | { 61 | id: integer("id").primaryKey({ autoIncrement: true }), 62 | name: text("name").notNull(), 63 | version: text("version"), 64 | commit_hash: text("commit_hash"), 65 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 66 | }, 67 | (table) => ({ 68 | runtimeUniqueIdx: uniqueIndex("runtime_unique_idx").on( 69 | table.name, 70 | table.version, 71 | table.commit_hash 72 | ), 73 | }) 74 | ); 75 | 76 | export const benchmarkSystems = sqliteTable( 77 | "benchmark_systems", 78 | { 79 | id: integer("id").primaryKey({ autoIncrement: true }), 80 | cpu_name: text("cpu_name"), 81 | cpu_arch: text("cpu_arch"), 82 | ram_gb: real("ram_gb"), 83 | kernel_type: text("kernel_type"), 84 | kernel_release: text("kernel_release"), 85 | system_version: text("system_version"), 86 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 87 | }, 88 | (table) => ({ 89 | systemUniqueIdx: uniqueIndex("system_unique_idx").on( 90 | table.cpu_name, 91 | table.cpu_arch, 92 | table.ram_gb, 93 | table.kernel_type, 94 | table.kernel_release, 95 | table.system_version 96 | ), 97 | }) 98 | ); 99 | 100 | export const benchmarkRuns = sqliteTable("benchmark_runs", { 101 | id: integer("id").primaryKey({ autoIncrement: true }), 102 | system_id: integer("system_id").references(() => benchmarkSystems.id), 103 | accelerator_id: integer("accelerator_id").references(() => accelerators.id), 104 | model_variant_id: integer("model_variant_id").references( 105 | () => modelVariants.id 106 | ), 107 | runtime_id: integer("runtime_id").references(() => runtimes.id), 108 | avg_prompt_tps: real("avg_prompt_tps").notNull(), 109 | avg_gen_tps: real("avg_gen_tps").notNull(), 110 | avg_ttft_ms: real("avg_ttft_ms").notNull(), 111 | performance_score: real("performance_score").notNull(), 112 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 113 | }); 114 | 115 | export const testResults = sqliteTable("test_results", { 116 | id: integer("id").primaryKey({ autoIncrement: true }), 117 | benchmark_run_id: integer("benchmark_run_id").references( 118 | () => benchmarkRuns.id 119 | ), 120 | name: text("name"), 121 | n_prompt: integer("n_prompt"), 122 | n_gen: integer("n_gen"), 123 | avg_time_ms: real("avg_time_ms"), 124 | power_watts: real("power_watts"), 125 | prompt_tps: real("prompt_tps"), 126 | gen_tps: real("gen_tps"), 127 | prompt_tps_watt: real("prompt_tps_watt"), 128 | gen_tps_watt: real("gen_tps_watt"), 129 | vram_used_mb: real("vram_used_mb"), 130 | ttft_ms: real("ttft_ms"), 131 | created_at: integer("created_at").default(sql`(CURRENT_TIMESTAMP)`), 132 | }); 133 | 134 | export const acceleratorModelPerformanceScores = sqliteTable( 135 | "accelerator_model_performance_scores", 136 | { 137 | accelerator_id: integer("accelerator_id") 138 | .notNull() 139 | .references(() => accelerators.id), 140 | accelerator_name: text("accelerator_name").notNull(), 141 | accelerator_type: text("accelerator_type").notNull(), 142 | accelerator_memory_gb: real("accelerator_memory_gb").notNull(), 143 | model_id: integer("model_id") 144 | .notNull() 145 | .references(() => models.id), 146 | model_name: text("model_name").notNull(), 147 | model_variant_id: integer("model_variant_id") 148 | .notNull() 149 | .references(() => modelVariants.id), 150 | model_variant_quant: text("model_variant_quant").notNull(), 151 | avg_prompt_tps: real("avg_prompt_tps"), 152 | avg_gen_tps: real("avg_gen_tps"), 153 | avg_ttft: real("avg_ttft"), 154 | performance_score: real("performance_score"), 155 | }, 156 | (table) => ({ 157 | acceleratorModelPerformanceScoresUniqueIdx: uniqueIndex( 158 | "accelerator_model_performance_scores_unique_idx" 159 | ).on(table.accelerator_id, table.model_id, table.model_variant_id), 160 | }) 161 | ); 162 | 163 | export type DbAccelerator = typeof accelerators.$inferSelect; 164 | export type DbBenchmarkRun = typeof benchmarkRuns.$inferSelect; 165 | export type DbBenchmarkSystem = typeof benchmarkSystems.$inferSelect; 166 | export type DbModel = typeof models.$inferSelect; 167 | export type DbModelVariant = typeof modelVariants.$inferSelect; 168 | export type DbRuntime = typeof runtimes.$inferSelect; 169 | export type DbTestResult = typeof testResults.$inferSelect; 170 | export type DbAcceleratorModelPerformanceScore = 171 | typeof acceleratorModelPerformanceScores.$inferSelect; 172 | -------------------------------------------------------------------------------- /src/lib/selectStyles.ts: -------------------------------------------------------------------------------- 1 | import { Theme } from "react-select"; 2 | 3 | const scrollbarStyles = { 4 | "&::-webkit-scrollbar": { 5 | width: "8px", 6 | }, 7 | "&::-webkit-scrollbar-track": { 8 | background: "var(--background-scrollbar)", 9 | borderRadius: "4px", 10 | }, 11 | "&::-webkit-scrollbar-thumb": { 12 | background: "var(--scrollbar-thumb)", 13 | borderRadius: "4px", 14 | "&:hover": { 15 | background: "var(--primary-500)", 16 | }, 17 | }, 18 | }; 19 | 20 | const commonStyles = { 21 | menu: (base: any) => ({ 22 | ...base, 23 | marginTop: 0, 24 | backgroundColor: "var(--background-menu)", 25 | border: "none", 26 | borderRadius: "0 0 8px 8px", 27 | boxShadow: "0px 14px 84px -14px rgba(185, 161, 252, 0.6)", 28 | ...scrollbarStyles, 29 | }), 30 | 31 | menuList: (base: any) => ({ 32 | ...base, 33 | ...scrollbarStyles, 34 | padding: 0, 35 | borderRadius: "0 0 8px 8px", 36 | }), 37 | 38 | option: (base: any, { isFocused, isSelected }: any) => ({ 39 | ...base, 40 | backgroundColor: isFocused 41 | ? "var(--primary-500)" 42 | : "var(--background-menu)", 43 | color: isFocused ? "white" : isSelected ? "var(--primary-500)" : "inherit", 44 | cursor: "pointer", 45 | padding: "10px 20px", 46 | borderBottom: "1px solid rgba(88, 42, 203, 0.1)", 47 | }), 48 | 49 | control: (base: any, { menuIsOpen }: any) => ({ 50 | ...base, 51 | background: menuIsOpen 52 | ? "var(--background-menu)" 53 | : "var(--background-menu)", 54 | "&:hover": { 55 | background: menuIsOpen 56 | ? "var(--background-menu)" 57 | : "var(--background-menu-hover)", 58 | }, 59 | boxShadow: "none", 60 | borderRadius: menuIsOpen ? "8px 8px 0 0" : "8px", 61 | cursor: menuIsOpen ? "default" : "pointer", 62 | }), 63 | }; 64 | 65 | // Specific styles for search component 66 | export const searchStyles = { 67 | dropdownIndicator: () => ({ 68 | display: "none", 69 | }), 70 | 71 | indicatorSeparator: () => ({ 72 | display: "none", 73 | }), 74 | 75 | menu: (base: any) => ({ 76 | ...commonStyles.menu(base), 77 | marginBottom: 0, 78 | padding: "0px 0px", 79 | clipPath: 80 | "polygon(-100% -50%, 0 -50%, 0 0, 100% 0, 100% -50%, 200% -50%, 200% 200%, -100% 200%)", 81 | }), 82 | 83 | menuList: (base: any) => ({ 84 | ...commonStyles.menuList(base), 85 | borderTop: "1px solid rgba(88, 42, 203, 0.1)", 86 | "& > div:last-child > div:last-child > div:last-child": { 87 | borderBottom: "none", 88 | }, 89 | }), 90 | 91 | container: (base: any, { isFocused }: any) => ({ 92 | ...base, 93 | borderRadius: isFocused ? "8px 8px 0 0" : "8px", 94 | backgroundColor: "var(--background-menu)", 95 | }), 96 | 97 | control: (base: any, props: any) => ({ 98 | ...commonStyles.control(base, props), 99 | border: "none", 100 | padding: "10px 20px", 101 | position: "relative", 102 | "&::before": { 103 | content: '""', 104 | position: "absolute", 105 | top: 0, 106 | left: 0, 107 | right: 0, 108 | bottom: 0, 109 | borderRadius: props.menuIsOpen ? "8px 8px 0 0" : "8px", 110 | boxShadow: props.menuIsOpen 111 | ? "0 14px 84px 0 rgba(185, 161, 252, 0.6)" 112 | : "none", 113 | zIndex: -1, 114 | }, 115 | }), 116 | 117 | input: (base: any) => ({ 118 | ...base, 119 | margin: 0, 120 | padding: 0, 121 | }), 122 | 123 | group: (base: any) => ({ 124 | ...base, 125 | padding: 0, 126 | }), 127 | 128 | groupHeading: (base: any) => ({ 129 | ...base, 130 | color: "var(--grey-400)", 131 | fontWeight: 500, 132 | padding: "10px 20px", 133 | borderBottom: "1px solid rgba(88, 42, 203, 0.1)", 134 | textTransform: "none", 135 | }), 136 | 137 | option: commonStyles.option, 138 | }; 139 | 140 | // Specific styles for multi-select component 141 | export const multiSelectStyles = { 142 | control: (base: any, { isFocused, menuIsOpen }: any) => ({ 143 | ...commonStyles.control(base, { menuIsOpen }), 144 | minHeight: "40px", 145 | border: isFocused 146 | ? "2px solid var(--primary-500)" 147 | : "2px solid var(--background-menu)", 148 | padding: "8px 2px", 149 | borderRadius: "8px", // Ensures all corners are rounded when menu is closed 150 | }), 151 | 152 | option: (base: any, props: any) => ({ 153 | ...commonStyles.option(base, props), 154 | padding: "10px 20px 10px 8px", 155 | }), 156 | 157 | container: (base: any) => ({ 158 | ...base, 159 | borderRadius: "none", 160 | }), 161 | 162 | menu: (base: any) => ({ 163 | ...commonStyles.menu(base), 164 | marginBottom: "-10px", 165 | marginLeft: "8px", 166 | width: "calc(100% - 16px)", 167 | boxShadow: "0px 22px 48px 0px rgba(185, 161, 252, 0.6)", 168 | clipPath: "polygon(-100% 0%, 1000% 0%, 200% 1000%, -100% 1000%)", 169 | }), 170 | 171 | menuList: (base: any) => ({ 172 | ...commonStyles.menuList(base), 173 | borderRadius: 8, 174 | "& > div:last-of-type": { 175 | borderBottom: "none", 176 | }, 177 | }), 178 | 179 | multiValue: (base: any) => ({ 180 | ...base, 181 | backgroundColor: "var(--primary-100)", 182 | gap: "8px", 183 | padding: "0px", 184 | borderRadius: "6px", 185 | }), 186 | 187 | multiValueLabel: (base: any) => ({ 188 | ...base, 189 | fontSize: "inherit", 190 | }), 191 | 192 | multiValueRemove: (base: any) => ({ 193 | ...base, 194 | color: "var(--primary-500)", 195 | padding: "0px 5px", 196 | borderRadius: "0px 6px 6px 0px", 197 | margin: 0, 198 | ":hover": { 199 | backgroundColor: "var(--primary-500)", 200 | color: "white", 201 | }, 202 | }), 203 | }; 204 | 205 | export const selectTheme = (theme: Theme) => ({ 206 | ...theme, 207 | colors: { 208 | ...theme.colors, 209 | // Clear indicator (X button) 210 | neutral60: "#582ACB", // Normal state 211 | neutral80: "#582ACB50", // Hovered state 212 | 213 | // Dropdown indicator (arrow) 214 | neutral20: "#582ACB30", // Separator color with 30% opacity 215 | neutral30: "#582ACB", // Hovered state for the dropdown indicator 216 | 217 | // Main colors 218 | primary: "#582ACB", // Used for focused elements 219 | primary25: "#582ACB30", // Option hover state with 30% opacity 220 | primary50: "#582ACB80", // Selected option - keeping this a bit more visible at 80% 221 | }, 222 | }); 223 | -------------------------------------------------------------------------------- /src/components/charts/MetricsBarChart.tsx: -------------------------------------------------------------------------------- 1 | import { MetricUnits } from "@/lib/constants"; 2 | import { PerformanceMetricKey } from "@/lib/types"; 3 | import { formatMetricValue } from "@/lib/utils"; 4 | import { useMediaQuery } from "react-responsive"; 5 | import { 6 | Bar, 7 | BarChart, 8 | Cell, 9 | Label, 10 | LabelProps, 11 | ResponsiveContainer, 12 | Tooltip, 13 | TooltipProps, 14 | XAxis, 15 | YAxis, 16 | } from "recharts"; 17 | import { 18 | NameType, 19 | ValueType, 20 | } from "recharts/types/component/DefaultTooltipContent"; 21 | 22 | export interface ChartDataItem { 23 | name: string; 24 | value: number; 25 | color: string; 26 | isHighlighted?: boolean; 27 | [key: string]: any; // For additional properties 28 | } 29 | 30 | interface MetricsChartProps { 31 | chartData: ChartDataItem[]; 32 | metricKey: PerformanceMetricKey; 33 | sortDirection?: "asc" | "desc"; 34 | xAxisLabel?: string; 35 | yAxisWidth?: number; 36 | hasHighlighting?: boolean; 37 | maxLabelLength?: number; 38 | chartType: "byModel" | "byAccelerator"; 39 | } 40 | 41 | const MetricsBarChart: React.FC = ({ 42 | chartData, 43 | metricKey, 44 | sortDirection = "desc", 45 | xAxisLabel = "", 46 | yAxisWidth = 160, 47 | hasHighlighting = false, 48 | maxLabelLength = 20, 49 | chartType, 50 | }) => { 51 | const isMobile = useMediaQuery({ maxWidth: 640 }); 52 | 53 | const BarLabel: React.FC = (props) => { 54 | const { x = 0, y = 0, width = 0, height = 0, value } = props; 55 | const numValue = typeof value === "string" ? parseFloat(value) : value ?? 0; 56 | return ( 57 | 65 | {formatMetricValue(metricKey, numValue).simple} 66 | 67 | ); 68 | }; 69 | 70 | const CustomTooltip: React.FC> = ({ 71 | active, 72 | payload, 73 | label, 74 | }) => { 75 | if (active && payload && payload.length) { 76 | return ( 77 |
86 |

{label}

87 | {payload.map((entry, index) => { 88 | // Handle different value types (number or string) 89 | const value = 90 | typeof entry.value === "number" 91 | ? entry.value.toFixed(1) 92 | : entry.value; 93 | 94 | return ( 95 |

99 | {`${value} ${MetricUnits[metricKey]}`} 100 |

101 | ); 102 | })} 103 |
104 | ); 105 | } 106 | 107 | return null; 108 | }; 109 | 110 | // Chart margins based on chart type 111 | const margins = { 112 | top: 20, 113 | right: isMobile ? 20 : chartType === "byModel" ? 50 : 40, 114 | left: isMobile 115 | ? chartType === "byAccelerator" 116 | ? -80 117 | : -20 118 | : chartType === "byAccelerator" 119 | ? -20 120 | : 0, 121 | bottom: 40, 122 | }; 123 | 124 | return ( 125 | 129 | 130 | 131 | {xAxisLabel === "none" ? null : ( 132 | 144 | { 149 | const dataItem = chartData.find( 150 | (item) => item.name === payload.value 151 | ); 152 | const isHighlighted = dataItem?.isHighlighted; 153 | const text = payload.value; 154 | const lines: string[] = []; 155 | 156 | // Split text into lines (keeping your existing logic) 157 | let remainingText = text; 158 | while (remainingText.length > 0) { 159 | if (remainingText.length <= maxLabelLength) { 160 | lines.push(remainingText); 161 | break; 162 | } 163 | 164 | const spaceIndex = remainingText.lastIndexOf(" ", maxLabelLength); 165 | if (spaceIndex === -1) { 166 | lines.push(remainingText.substring(0, maxLabelLength)); 167 | remainingText = remainingText.substring(maxLabelLength); 168 | } else { 169 | lines.push(remainingText.substring(0, spaceIndex)); 170 | remainingText = remainingText.substring(spaceIndex + 1); 171 | } 172 | } 173 | 174 | // Calculate vertical alignment 175 | const lineHeight = 16; // Adjust this based on your font size 176 | const totalHeight = (lines.length - 1) * lineHeight; 177 | const verticalOffset = -totalHeight / 2 + 3; // Center the block vertically 178 | 179 | return ( 180 | 181 | {lines.map((line, index) => ( 182 | 192 | {line} 193 | 194 | ))} 195 | 196 | ); 197 | }} 198 | /> 199 | } 201 | wrapperStyle={{ outline: "none" }} 202 | /> 203 | }> 204 | {chartData.map((entry, index) => ( 205 | 213 | ))} 214 | 215 | 216 | 217 | ); 218 | }; 219 | 220 | export default MetricsBarChart; 221 | -------------------------------------------------------------------------------- /src/pages/accelerator/[id]/index.tsx: -------------------------------------------------------------------------------- 1 | import PageHeader from "@/components/layout/PageHeader"; 2 | import Separator from "@/components/ui/Separator"; 3 | import { 4 | getAcceleratorsById, 5 | getPerforamanceModelVariantsByAcceleratorId, 6 | getPerformanceScores, 7 | } from "@/db/queries"; 8 | import { OFFICIAL_MODELS } from "@/lib/config"; 9 | import { 10 | Accelerator, 11 | PerformanceMetricKey, 12 | PerformanceScore, 13 | } from "@/lib/types"; 14 | import { formatMetricValue, getModelParamsString } from "@/lib/utils"; 15 | import { GetServerSideProps } from "next"; 16 | import React from "react"; 17 | import ModelCompareCard from "@/components/cards/compare/ModelCompareCard"; 18 | import Card from "@/components/ui/Card"; 19 | import AcceleratorInfo from "@/components/display/AcceleratorInfo"; 20 | import CardHeader from "@/components/ui/CardHeader"; 21 | import Meta from "@/components/layout/Meta"; 22 | 23 | const ModelInfo = ({ result }: { result: PerformanceScore }) => ( 24 |
25 |
26 | {result.model.name} 27 |
28 |
29 | {result.model.quant} 30 | {getModelParamsString(result.model.params)} 31 |
32 |
33 | ); 34 | 35 | const MetricValue = ({ 36 | result, 37 | metricKey, 38 | }: { 39 | result: PerformanceScore; 40 | metricKey: PerformanceMetricKey; 41 | }) => { 42 | const value = result.results[0]?.[metricKey]; 43 | if (!value) return
N/A
; 44 | 45 | const formatted = formatMetricValue(metricKey, value); 46 | return ( 47 |
48 | {formatted.formatted} 49 | {formatted.suffix} 50 |
51 | ); 52 | }; 53 | 54 | const MetricRow = ({ 55 | label, 56 | results, 57 | metricKey, 58 | }: { 59 | label: string; 60 | results: PerformanceScore[]; 61 | metricKey: PerformanceMetricKey; 62 | }) => ( 63 |
64 |
65 | {label} 66 |
67 | {results.map((result, i) => ( 68 | 69 | ))} 70 |
71 | ); 72 | 73 | const LocalScoreRow = ({ results }: { results: PerformanceScore[] }) => ( 74 |
75 |
LocalScore
76 | {results.map((result, i) => ( 77 |
78 | {result.results[0]?.performance_score ? ( 79 | 80 | { 81 | formatMetricValue( 82 | "performance_score", 83 | result.results[0]?.performance_score 84 | ).formatted 85 | } 86 | 87 | ) : ( 88 | "N/A" 89 | )} 90 |
91 | ))} 92 |
93 | ); 94 | 95 | const AcceleratorPerformanceOverview = ({ 96 | results, 97 | }: { 98 | results: PerformanceScore[]; 99 | }) => { 100 | const sortedResults = [...results].sort((a, b) => { 101 | if (a.model.params < b.model.params) return -1; 102 | if (a.model.params > b.model.params) return 1; 103 | return 0; 104 | }); 105 | 106 | return ( 107 | 108 | 109 | 110 |
111 |
Model
112 | {sortedResults.map((result, i) => ( 113 | 114 | ))} 115 |
116 | 117 | 118 | 119 | 124 | 129 | 134 | 135 | 136 | 137 | 138 |
139 | ); 140 | }; 141 | 142 | const AcceleratorPage = ({ 143 | accelInfo, 144 | officialModelResults, 145 | results, 146 | id, 147 | }: { 148 | accelInfo: Accelerator | null; 149 | officialModelResults: PerformanceScore[]; 150 | results: PerformanceScore[]; 151 | id: string; 152 | }) => { 153 | if (!accelInfo) { 154 | return
Accelerator not found
; 155 | } 156 | 157 | return ( 158 | <> 159 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | ); 175 | }; 176 | 177 | export const getServerSideProps: GetServerSideProps = async (context) => { 178 | const startTime = Date.now(); 179 | 180 | const { id: idRaw } = context.query; 181 | const id = idRaw as string; 182 | 183 | const acceleratorId = parseInt(id); 184 | 185 | const accelInfo = await getAcceleratorsById(acceleratorId); 186 | const modelVariantIds = await getPerforamanceModelVariantsByAcceleratorId( 187 | acceleratorId 188 | ); 189 | 190 | const modelResults = await getPerformanceScores( 191 | [acceleratorId], 192 | modelVariantIds 193 | ); 194 | 195 | const endTime = Date.now(); 196 | console.log(`/accelerator/${id} DB fetch took ${endTime - startTime}ms`); 197 | 198 | const normalizedModelResults = OFFICIAL_MODELS.map((model) => { 199 | const existingResult = modelResults.find( 200 | (result) => result.model.name === model.name 201 | ); 202 | return existingResult; 203 | }).filter((result) => result !== undefined); 204 | 205 | if (normalizedModelResults.length < 3) { 206 | // Fill in normalizedModelResults with models that are not in the official list 207 | const nonOfficialResults = modelResults.filter( 208 | (result) => 209 | !OFFICIAL_MODELS.some( 210 | (officialModel) => officialModel.name === result.model.name 211 | ) 212 | ); 213 | 214 | // Add enough non-official models to reach at least 3 models in normalizedModelResults 215 | const numberOfModelsToAdd = Math.min( 216 | 3 - normalizedModelResults.length, 217 | nonOfficialResults.length 218 | ); 219 | if (numberOfModelsToAdd > 0) { 220 | normalizedModelResults.push( 221 | ...nonOfficialResults.slice(0, numberOfModelsToAdd) 222 | ); 223 | } 224 | } 225 | 226 | return { 227 | props: { 228 | officialModelResults: normalizedModelResults, 229 | results: modelResults, 230 | accelInfo: accelInfo ?? null, 231 | id, 232 | }, 233 | }; 234 | }; 235 | 236 | export default AcceleratorPage; 237 | -------------------------------------------------------------------------------- /src/components/layout/SearchBar.tsx: -------------------------------------------------------------------------------- 1 | import { fetcher } from "@/lib/swr"; 2 | import { SearchBarOption, SearchResponse } from "@/lib/types"; 3 | import { useRouter } from "next/router"; 4 | import React, { useState, useCallback, useEffect } from "react"; 5 | import dynamic from "next/dynamic"; 6 | 7 | const Select = dynamic(() => import("react-select"), { ssr: false }); 8 | // const AsyncSelect = dynamic(() => import("react-select/async"), { ssr: false }); 9 | import { 10 | components, 11 | GroupBase, 12 | InputProps, 13 | OptionProps, 14 | OptionsOrGroups, 15 | } from "react-select"; 16 | import useSWR from "swr"; 17 | import AcceleratorSelectOptionLabel from "../select/AcceleratorSelectOptionLabel"; 18 | import ModelSelectOptionLabel from "../select/ModelSelectOptionLabel"; 19 | import SearchIcon from "../icons/SearchIcon"; 20 | import { searchStyles } from "@/lib/selectStyles"; 21 | 22 | const getOptionsFromResponse = ( 23 | data: SearchResponse 24 | ): OptionsOrGroups> => { 25 | if (!data.models && !data.accelerators) { 26 | return []; 27 | } 28 | 29 | const modelOptions = data.models.map((model) => ({ 30 | value: `${model.variantId}`, 31 | group: "model" as const, 32 | model: model, 33 | })); 34 | 35 | const acceleratorOptions = data.accelerators.map((acc) => ({ 36 | value: `${acc.id}`, 37 | group: "accelerator" as const, 38 | accelerator: acc, 39 | })); 40 | 41 | return [ 42 | { 43 | label: "Models", 44 | options: modelOptions, 45 | }, 46 | { 47 | label: "Accelerators", 48 | options: acceleratorOptions, 49 | }, 50 | ]; 51 | }; 52 | 53 | const CustomInput = ( 54 | props: InputProps> 55 | ) => { 56 | const { value, selectProps } = props; 57 | const menuIsOpen = selectProps.menuIsOpen; 58 | 59 | return ( 60 |
61 | 62 | 63 | {value === "" && !menuIsOpen && ( 64 |

Search

65 | )} 66 | 67 | 72 |
73 | ); 74 | }; 75 | 76 | const CustomOption = (props: OptionProps) => { 77 | const { data, isFocused } = props; 78 | 79 | // Based on the option type, render different content 80 | if (data.group === "model") { 81 | return ( 82 | 83 | 84 | 85 | ); 86 | } else if (data.group === "accelerator") { 87 | return ( 88 | 89 | 93 | 94 | ); 95 | } 96 | 97 | // Fallback to default rendering 98 | return ; 99 | }; 100 | 101 | // Custom hook for debouncing 102 | const useDebounce = (value: string, delay: number): string => { 103 | const [debouncedValue, setDebouncedValue] = useState(value); 104 | 105 | useEffect(() => { 106 | const timer = setTimeout(() => { 107 | setDebouncedValue(value); 108 | }, delay); 109 | 110 | return () => clearTimeout(timer); 111 | }, [value, delay]); 112 | 113 | return debouncedValue; 114 | }; 115 | 116 | export const SearchBar: React.FC<{ className?: string }> = ({ className }) => { 117 | const router = useRouter(); 118 | const [inputValue, setInputValue] = useState(""); 119 | const [isNavigating, setIsNavigating] = useState(false); 120 | const [selectedOption, setSelectedOption] = useState( 121 | null 122 | ); 123 | const [displayedOptions, setDisplayedOptions] = useState< 124 | OptionsOrGroups> 125 | >([]); 126 | 127 | // Use the debounce hook instead of manual debouncing 128 | const debouncedQuery = useDebounce(inputValue, 300); 129 | 130 | // Use SWR for data fetching and caching 131 | const { data, isValidating } = useSWR( 132 | `/api/search?q=${encodeURIComponent(debouncedQuery)}`, 133 | fetcher, 134 | { 135 | dedupingInterval: 5 * 60 * 1000, // 5 minutes cache 136 | revalidateOnFocus: false, 137 | keepPreviousData: true, // Important: Keep previous data while loading new data 138 | } 139 | ); 140 | 141 | // Update displayed options only when new data arrives 142 | useEffect(() => { 143 | if (data && !data.error) { 144 | const processedOptions = getOptionsFromResponse(data); 145 | if (processedOptions) { 146 | setDisplayedOptions(processedOptions); 147 | } 148 | } 149 | }, [data]); 150 | 151 | // Set up router change event listeners 152 | useEffect(() => { 153 | const handleRouteChangeStart = () => { 154 | setIsNavigating(true); 155 | }; 156 | 157 | const handleRouteChangeComplete = () => { 158 | setIsNavigating(false); 159 | setSelectedOption(null); 160 | }; 161 | 162 | const handleRouteChangeError = () => { 163 | setIsNavigating(false); 164 | setSelectedOption(null); 165 | }; 166 | 167 | router.events.on("routeChangeStart", handleRouteChangeStart); 168 | router.events.on("routeChangeComplete", handleRouteChangeComplete); 169 | router.events.on("routeChangeError", handleRouteChangeError); 170 | 171 | return () => { 172 | router.events.off("routeChangeStart", handleRouteChangeStart); 173 | router.events.off("routeChangeComplete", handleRouteChangeComplete); 174 | router.events.off("routeChangeError", handleRouteChangeError); 175 | }; 176 | }, [router]); 177 | 178 | const handleInputChange = (newValue: string) => { 179 | setInputValue(newValue); 180 | }; 181 | 182 | const handleOptionSelect = useCallback( 183 | (option: SearchBarOption | null) => { 184 | if (!option) return; 185 | 186 | setSelectedOption(option); 187 | setIsNavigating(true); 188 | 189 | const path = 190 | option.group === "model" 191 | ? `/model/${option.model?.variantId}` 192 | : `/accelerator/${option.accelerator?.id}`; 193 | 194 | router.push(path); 195 | }, 196 | [router] 197 | ); 198 | 199 | return ( 200 | // @ts-ignore - for some reason the dynamic import is causing a type error 201 | > 202 | cacheOptions 203 | aria-label="Search Models and Accelerators" 204 | className={`w-full ${className}`} 205 | styles={searchStyles} 206 | onChange={handleOptionSelect} 207 | options={displayedOptions} 208 | value={selectedOption} 209 | isLoading={isValidating || isNavigating} 210 | onInputChange={handleInputChange} 211 | noOptionsMessage={() => "No Results"} 212 | blurInputOnSelect={true} 213 | components={{ 214 | Input: CustomInput, 215 | Option: CustomOption, 216 | }} 217 | placeholder={null} 218 | filterOption={(option, inputValue) => { 219 | const searchTerm = inputValue.toLowerCase(); 220 | const data = option.data as SearchBarOption; 221 | return ( 222 | data.model?.name.toLowerCase().includes(searchTerm) || 223 | data.accelerator?.name.toLowerCase().includes(searchTerm) 224 | ); 225 | }} 226 | /> 227 | ); 228 | }; 229 | 230 | export default SearchBar; 231 | -------------------------------------------------------------------------------- /src/pages/api/results.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | 3 | import { getBenchmarkResults, updatePerformanceScores } from "@/db/queries"; 4 | import db from "@/db"; 5 | import type { NextApiRequest, NextApiResponse } from "next"; 6 | import { z } from "zod"; 7 | import { 8 | accelerators, 9 | benchmarkRuns, 10 | benchmarkSystems, 11 | models, 12 | modelVariants, 13 | runtimes, 14 | testResults, 15 | } from "@/db/schema"; 16 | import { sql } from "drizzle-orm"; 17 | 18 | const DEFAULT_PAGE_SIZE = 20; 19 | const MAX_PAGE_SIZE = 100; 20 | const MIN_PAGE_SIZE = 1; 21 | 22 | const AcceleratorTypeEnum = z.enum(["CPU", "GPU", "TPU"]); 23 | 24 | const SystemInfoSchema = z.object({ 25 | cpu_name: z.string(), 26 | cpu_arch: z.string(), 27 | ram_gb: z.number(), 28 | kernel_type: z.string(), 29 | kernel_release: z.string(), 30 | version: z.string(), 31 | }); 32 | 33 | const AcceleratorSchema = z.object({ 34 | name: z.string(), 35 | type: AcceleratorTypeEnum, 36 | memory_gb: z.number(), 37 | manufacturer: z.string().nullable(), 38 | }); 39 | 40 | const RuntimeSchema = z.object({ 41 | name: z.string(), 42 | version: z.string().optional(), 43 | commit: z.string().optional(), 44 | }); 45 | 46 | const TestResultSchema = z.object({ 47 | name: z.string().max(255), 48 | model_name: z.string().max(255).min(1), 49 | model_quant_str: z.string().max(255).min(1), 50 | model_params_str: z.string().max(255).min(0), 51 | model_n_params: z.number().int(), 52 | n_prompt: z.number().int(), 53 | n_gen: z.number().int(), 54 | avg_time_ms: z.number(), 55 | prompt_tps: z.number(), 56 | prompt_tps_watt: z.number(), 57 | gen_tps: z.number(), 58 | gen_tps_watt: z.number(), 59 | power_watts: z.number(), 60 | ttft_ms: z.number(), 61 | }); 62 | 63 | const TestResultSummarySchema = z.object({ 64 | avg_prompt_tps: z.number(), 65 | avg_gen_tps: z.number(), 66 | avg_ttft_ms: z.number(), 67 | performance_score: z.number(), 68 | }); 69 | 70 | const StoreBenchmarkResultsRequestSchema = z 71 | .object({ 72 | system_info: SystemInfoSchema, 73 | accelerator_info: AcceleratorSchema, 74 | runtime_info: RuntimeSchema, 75 | results_summary: TestResultSummarySchema, 76 | results: z.array(TestResultSchema).min(1), 77 | }) 78 | .refine((data) => { 79 | if (data.results.length === 0) return true; 80 | const firstResult = data.results[0]; 81 | return data.results.every( 82 | (result) => 83 | result.model_name === firstResult.model_name && 84 | result.model_quant_str === firstResult.model_quant_str && 85 | result.model_n_params === firstResult.model_n_params 86 | ); 87 | }, "All results must have the same model_name, model_quant_str, model_n_params"); 88 | 89 | function ensureSingleResult(results: T[]): T { 90 | if (results.length !== 1) { 91 | throw new Error(`Expected exactly one result, but got ${results.length}`); 92 | } 93 | return results[0]; 94 | } 95 | 96 | export default async function handler( 97 | req: NextApiRequest, 98 | res: NextApiResponse 99 | ) { 100 | switch (req.method) { 101 | case "GET": 102 | return GET(req, res); 103 | case "POST": 104 | return POST(req, res); 105 | default: 106 | res.setHeader("Allow", ["GET", "POST"]); 107 | return res.status(405).end(`Method ${req.method} Not Allowed`); 108 | } 109 | } 110 | 111 | async function GET(req: NextApiRequest, res: NextApiResponse) { 112 | // Get pagination parameters from query 113 | const page = Math.max(1, parseInt(req.query.page as string) || 1); 114 | let limit = parseInt(req.query.limit as string) || DEFAULT_PAGE_SIZE; 115 | 116 | // Enforce limit boundaries 117 | limit = Math.min(MAX_PAGE_SIZE, Math.max(MIN_PAGE_SIZE, limit)); 118 | 119 | // Calculate offset 120 | const offset = (page - 1) * limit; 121 | 122 | // Get sort direction from query 123 | const sortDirection = req.query.sortDirection === "asc" ? "asc" : "desc"; 124 | 125 | const results = await getBenchmarkResults({ sortDirection, limit, offset }); 126 | 127 | res.status(200).json(results); 128 | } 129 | 130 | async function POST(req: NextApiRequest, res: NextApiResponse) { 131 | const parse = StoreBenchmarkResultsRequestSchema.safeParse(req.body); 132 | if (!parse.success) { 133 | console.log("parse error", parse.error); 134 | res.status(400).json({ error: parse.error }); 135 | return; 136 | } 137 | 138 | const data = parse.data; 139 | 140 | const modelName = data.results[0].model_name; 141 | const modelParams = data.results[0].model_n_params; 142 | const modelQuantStr = data.results[0].model_quant_str; 143 | 144 | const benchmarkRunUuid = await db.transaction(async (tx) => { 145 | // const accelerator_id 146 | 147 | try { 148 | // insert the system 149 | const sysResult = await tx 150 | .insert(benchmarkSystems) 151 | .values({ 152 | ...data.system_info, 153 | system_version: data.system_info.version, 154 | }) 155 | .onConflictDoUpdate({ 156 | target: [ 157 | benchmarkSystems.cpu_name, 158 | benchmarkSystems.cpu_arch, 159 | benchmarkSystems.ram_gb, 160 | benchmarkSystems.kernel_type, 161 | benchmarkSystems.kernel_release, 162 | benchmarkSystems.system_version, 163 | ], 164 | set: { 165 | id: sql`${benchmarkSystems.id}`, 166 | }, 167 | }) 168 | .returning(); 169 | const system = ensureSingleResult(sysResult); 170 | 171 | // insert or get the accelerator 172 | const accelResult = await tx 173 | .insert(accelerators) 174 | .values({ 175 | name: data.accelerator_info.name, 176 | type: data.accelerator_info.type, 177 | memory_gb: data.accelerator_info.memory_gb, 178 | manufacturer: data.accelerator_info.manufacturer, 179 | }) 180 | .onConflictDoUpdate({ 181 | target: [ 182 | accelerators.name, 183 | accelerators.type, 184 | accelerators.memory_gb, 185 | ], 186 | set: { 187 | id: sql`${accelerators.id}`, 188 | }, 189 | }) 190 | .returning(); 191 | 192 | const accelerator = ensureSingleResult(accelResult); 193 | 194 | const runtimeResult = await tx 195 | .insert(runtimes) 196 | .values({ 197 | ...data.runtime_info, 198 | commit_hash: data.runtime_info.commit, 199 | }) 200 | .onConflictDoUpdate({ 201 | target: [runtimes.name, runtimes.version, runtimes.commit_hash], 202 | set: { 203 | id: sql`${runtimes.id}`, 204 | }, 205 | }) 206 | .returning(); 207 | const runtime = ensureSingleResult(runtimeResult); 208 | 209 | // insert or get the model 210 | const modelResult = await tx 211 | .insert(models) 212 | .values({ 213 | name: modelName, 214 | params: modelParams, 215 | }) 216 | .onConflictDoUpdate({ 217 | target: models.name, 218 | set: { 219 | id: sql`${models.id}`, 220 | }, 221 | }) 222 | .returning(); 223 | const model = ensureSingleResult(modelResult); 224 | 225 | const modelVariantResult = await tx 226 | .insert(modelVariants) 227 | .values({ 228 | model_id: model.id, 229 | quantization: modelQuantStr, 230 | }) 231 | .onConflictDoUpdate({ 232 | target: [modelVariants.model_id, modelVariants.quantization], 233 | set: { 234 | id: sql`${modelVariants.id}`, 235 | }, 236 | }) 237 | .returning(); 238 | const modelVariant = ensureSingleResult(modelVariantResult); 239 | 240 | const benchmarkRunResult = await tx 241 | .insert(benchmarkRuns) 242 | .values({ 243 | system_id: system.id, 244 | accelerator_id: accelerator.id, 245 | model_variant_id: modelVariant.id, 246 | runtime_id: runtime.id, 247 | avg_prompt_tps: data.results_summary.avg_prompt_tps, 248 | avg_gen_tps: data.results_summary.avg_gen_tps, 249 | avg_ttft_ms: data.results_summary.avg_ttft_ms, 250 | performance_score: data.results_summary.performance_score, 251 | }) 252 | .returning(); 253 | const benchmarkRun = ensureSingleResult(benchmarkRunResult); 254 | 255 | const insertResults = data.results.map((result) => ({ 256 | benchmark_run_id: benchmarkRun.id, 257 | ...result, 258 | })); 259 | 260 | await tx.insert(testResults).values(insertResults); 261 | 262 | // update the performance scores table 263 | await updatePerformanceScores(tx, model, modelVariant, accelerator); 264 | 265 | return benchmarkRun.id; 266 | } catch (e) { 267 | console.log("error", e); 268 | res.status(500).json({ error: e }); 269 | } 270 | }); 271 | 272 | res.status(200).json({ id: benchmarkRunUuid }); 273 | } 274 | -------------------------------------------------------------------------------- /src/pages/about/index.tsx: -------------------------------------------------------------------------------- 1 | import Meta from "@/components/layout/Meta"; 2 | import PageHeader from "@/components/layout/PageHeader"; 3 | import Hyperlink from "@/components/ui/Hyperlink"; 4 | import Separator from "@/components/ui/Separator"; 5 | import React from "react"; 6 | import Image from "next/image"; 7 | import TestDescriptionsTable from "../../components/informational/TestDescriptionsTable"; 8 | 9 | const AboutHeader = ({ 10 | text, 11 | className = "", 12 | }: { 13 | text: string; 14 | className?: string; 15 | }) => { 16 | return ( 17 |
18 |

{text}

19 | 20 |
21 | ); 22 | }; 23 | 24 | const AboutPage = () => { 25 | return ( 26 | <> 27 | 31 | About 32 | 33 |

34 | LocalScore is an open-source benchmarking tool designed to measure how 35 | fast Large Language Models (LLMs) run on your specific hardware. It is 36 | also a public database for the benchmark results. 37 |

38 |

39 | {`Whether you're wondering if your computer can smoothly run an 8 billion 40 | parameter model or trying to decide which GPU to buy for your local AI 41 | setup, LocalScore provides the data you need to make informed decisions.`} 42 |

43 |

44 | The benchmark results are meant to be directly comparable to each other. 45 | They also should give a fairly good indication of what real world 46 | performance you may see on your hardware. Unfortunately the benchmark 47 | suite cannot cover all possible scenarios (speculative decoding, etc), 48 | but it should give a rough idea of how well your hardware will perform. 49 |

50 | 51 |

52 | LocalScore measures three key performance metrics for local LLM 53 | performance. 54 |

55 |
    56 |
  1. 57 | 58 | Prompt Processing Speed: 59 | 60 | 61 | How quickly your system processes input text (tokens per second) 62 | 63 |
  2. 64 | 65 |
  3. 66 | 67 | Generation Speed: 68 | 69 | 70 | How fast your system generates new text (tokens per second) 71 | 72 |
  4. 73 | 74 |
  5. 75 | 76 | Time to First Token: 77 | 78 | 79 | The latency before the first response appears (milliseconds) 80 | 81 |
  6. 82 |
83 |

84 | These metrics are combined into a single LocalScore which gives 85 | you a straightforward way to compare different hardware configurations. 86 |

87 |

88 | A score of 1,000 is excellent, 250 is passable, and below 100 will 89 | likely be a poor user experience in some regard. 90 |

91 |

92 | Under the hood, LocalScore leverages{" "} 93 | 94 | Llamafile 95 | {" "} 96 | to ensure portability across different systems, making benchmarking 97 | accessible regardless of your setup. 98 |

99 | 100 |

101 | LocalScore has a straightforward test suite which is meant to emulate 102 | many common LLM tasks. The details of the tests are found in the table 103 | below: 104 |

105 | 106 | 107 |
    108 |
  1. 109 | Download LocalScore 110 |
  2. 111 |
  3. Run the benchmark and view your results
  4. 112 |
  5. 113 | Optionally submit your results to our public database to help the 114 | community 115 |
  6. 116 |
117 |

118 | When you submit your results, they become part of our growing database 119 | of hardware performance profiles, helping others understand what they 120 | can expect from similar setups. 121 |

122 |

123 | We collect the following non personally identifiable system information: 124 |

125 |
    126 |
  • 127 | Operating System Info: 128 | Name, Version, Release 129 |
  • 130 | 131 |
  • 132 | CPU Info: 133 | Name, Architecture 134 |
  • 135 |
  • 136 | RAM Info: 137 | Capacity 138 |
  • 139 |
  • 140 | GPU Info: 141 | Name, Manufacturer, Total Memory 142 |
  • 143 |
144 | 145 |

LocalScore currently supports:

{" "} 146 |
    147 |
  • 148 | CPUs 149 | (x86 and ARM) 150 |
  • 151 |
  • NVIDIA GPUs
  • 152 |
  • AMD GPUs
  • 153 |
  • 154 | Apple Silicon 155 | (M1/M2/etc) 156 |
  • 157 |
158 |

159 | The benchmark currently only supports single-GPU setups, which we 160 | believe represents the most practical approach for most users running 161 | LLMs locally. Similar to how gaming has shifted predominately to single 162 | GPU setups. In the future we may support multi-GPU setups. 163 |

164 | 165 |

166 | {`Due to limitations with 167 | Windows, you can't run Llamafile's which are larger than 4GB directly. 168 | Instead, you'll need to use LocalScore as a standalone utility and pass 169 | in your models in GGUF format to the benchmarking application.`} 170 |

171 | 172 |
173 |

174 | LocalScore is a 175 | 176 | Mozilla Builders 177 | 178 | 179 | {" "} 180 | Project. It is a free and accessible resource for the local AI 181 | community. It builds upon the excellent work of{" "} 182 | 183 | 184 | llama.cpp 185 | {" "} 186 | and{" "} 187 | 188 | Llamafile 189 | 190 | . 191 |

192 | 193 | Mozilla Logo 200 | 201 |
202 |

203 | {`We welcome contributions, suggestions, and feedback from the community. 204 | Whether you're interested in improving the benchmarking methodology, 205 | adding support for new hardware/models, or enhancing the user experience, your 206 | involvement is appreciated.`} 207 |

208 |

209 | Join us in creating a transparent, useful resource that helps everyone 210 | make the most of running LLMs on local hardware. 211 |

212 | 213 |

214 | You can find the code for the 215 | 216 | LocalScore CLI on GitHub 217 | 218 | 219 | {" "} 220 | along with detailed documentation, command-line options, and 221 | installation instructions. The code for the LocalScore website can be 222 | found in this{" "} 223 | 224 | 225 | GitHub 226 | {" "} 227 | repo. 228 |

229 | 230 | ); 231 | }; 232 | 233 | export default AboutPage; 234 | -------------------------------------------------------------------------------- /src/pages/result/[id].tsx: -------------------------------------------------------------------------------- 1 | import AcceleratorInfo from "@/components/display/AcceleratorInfo"; 2 | import Card from "@/components/ui/Card"; 3 | import ModelInfo from "@/components/display/ModelInfo"; 4 | import RuntimeInfo from "@/components/display/RuntimeInfo"; 5 | import Separator from "@/components/ui/Separator"; 6 | import SystemInfo from "@/components/display/SystemInfo"; 7 | import { 8 | getBenchmarkResult, 9 | getAcceleratorsPerformanceByModelVariant, 10 | getPerformanceScores, 11 | } from "@/db/queries"; 12 | import { 13 | DetailedRun, 14 | PerformanceMetricKey, 15 | PerformanceScore, 16 | } from "@/lib/types"; 17 | import { formatMetricValue } from "@/lib/utils"; 18 | import dayjs from "dayjs"; 19 | import timezone from "dayjs/plugin/timezone"; 20 | import utc from "dayjs/plugin/utc"; 21 | import { GetServerSideProps } from "next"; 22 | import React, { useState } from "react"; 23 | import PerformanceMetricGrid from "@/components/display/PerformanceResultsGrid"; 24 | import Meta from "@/components/layout/Meta"; 25 | import ModelMetricsChart from "@/components/charts/ModelMetricsChart"; 26 | import { MetricSortDirection } from "@/lib/constants"; 27 | import MetricSelector from "@/components/display/MetricSelector"; 28 | import Hyperlink from "@/components/ui/Hyperlink"; 29 | 30 | dayjs.extend(timezone); 31 | dayjs.extend(utc); 32 | 33 | interface SectionHeaderProps { 34 | title: string; 35 | } 36 | 37 | const SectionHeader: React.FC = ({ title }) => { 38 | return ( 39 |
40 | {title} 41 |
42 | ); 43 | }; 44 | 45 | // Component for page header with title and date 46 | interface PageHeaderProps { 47 | id: number; 48 | runDate: string; 49 | } 50 | 51 | const PageHeader: React.FC = ({ id, runDate }) => { 52 | return ( 53 |
54 |
55 | TEST #{id} RESULTS 56 |
57 |

58 | {dayjs.utc(runDate).local().format("MM/DD/YYYY - h:mm A")} 59 |

60 | 61 |
62 | ); 63 | }; 64 | 65 | // Component for consistently displaying formatted metric values 66 | interface MetricValueProps { 67 | metricType: PerformanceMetricKey; 68 | value: number; 69 | } 70 | 71 | const MetricValue: React.FC = ({ metricType, value }) => { 72 | const { formatted, suffix } = formatMetricValue(metricType, value); 73 | 74 | return ( 75 |
76 |
77 | {formatted} 78 |
79 |
{suffix}
80 |
81 | ); 82 | }; 83 | 84 | // Results table for detailed benchmark results 85 | interface ResultsTableProps { 86 | result: DetailedRun; 87 | } 88 | 89 | const ResultsTable: React.FC = ({ result }) => { 90 | return ( 91 |
92 |
93 |
TEST NAME
94 |
95 | PROMPT 96 |
97 |
98 | GENERATION 99 |
100 |
101 | TTFT 102 |
103 | 104 | 105 | 106 | {result.results.map((result) => ( 107 | 108 |
109 | {result.name} 110 |
111 | 115 | 116 | 117 | 118 |
119 | ))} 120 |
121 |
122 | ); 123 | }; 124 | 125 | const TestConfiguration = ({ result }: { result: DetailedRun }) => { 126 | return ( 127 |
128 |
129 | 130 | 136 |
137 | 138 |
139 | 140 | 141 |
142 |
143 | ); 144 | }; 145 | 146 | // Main page component 147 | const Page: React.FC<{ 148 | result: DetailedRun | null; 149 | compareResult: PerformanceScore | null; 150 | }> = ({ result, compareResult }) => { 151 | const [selectedKey, setSelectedKey] = 152 | useState("avg_gen_tps"); 153 | 154 | if (!result || !compareResult) { 155 | return
Result not found
; 156 | } 157 | 158 | return ( 159 |
160 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 |
172 | 173 |
174 | 175 | 176 | 177 |
178 | 179 | 180 | Explore All Results 181 | 182 |
183 |
184 |

185 | {result.model.name} - {result.model.quant} 186 |

187 |
188 | 192 |
193 | 203 |
204 | 205 | 206 | 207 |
208 | 209 | 210 |
211 | 212 | 213 | 214 |
215 | 216 | 217 |
218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 |
227 |
228 | ); 229 | }; 230 | 231 | export const getServerSideProps: GetServerSideProps = async (context) => { 232 | context.res.setHeader( 233 | "Cache-Control", 234 | "public, max-age=604800, stale-while-revalidate=604800" 235 | ); 236 | 237 | const startTime = Date.now(); 238 | 239 | const { id } = context.query; 240 | const runId = parseInt(id as string); 241 | 242 | const result = await getBenchmarkResult(runId); 243 | 244 | if (!result) { 245 | return { 246 | props: { 247 | result: null, 248 | compareResults: null, 249 | }, 250 | }; 251 | } 252 | 253 | const compareAccelIds = await getAcceleratorsPerformanceByModelVariant( 254 | result.model_variant_id 255 | ); 256 | 257 | const compareResults = await getPerformanceScores( 258 | [...compareAccelIds, result.accelerator_id], 259 | [result.model_variant_id] 260 | ); 261 | 262 | if (!compareResults || compareResults.length === 0) { 263 | return { 264 | props: { 265 | result: null, 266 | compareResults: null, 267 | }, 268 | }; 269 | } 270 | 271 | const compareResult = compareResults[0]; 272 | 273 | const foundResult = compareResult.results.find( 274 | (r) => r.accelerator.id === result.accelerator_id 275 | ); 276 | if (foundResult) { 277 | Object.assign(foundResult, { 278 | performance_score: result.performance_score, 279 | avg_prompt_tps: result.avg_prompt_tps, 280 | avg_gen_tps: result.avg_gen_tps, 281 | avg_ttft: result.avg_ttft_ms, 282 | accelerator: { 283 | ...foundResult.accelerator, 284 | name: `This System (${foundResult.accelerator.name})`, 285 | }, 286 | }); 287 | } 288 | 289 | const endTime = Date.now(); 290 | console.log(`/result/${id} DB fetch took ${endTime - startTime}ms`); 291 | 292 | return { 293 | props: { 294 | result, 295 | compareResult, 296 | }, 297 | }; 298 | }; 299 | 300 | export default Page; 301 | --------------------------------------------------------------------------------