SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
15.0 KB · 247 lines tsx
Raw Blame History
1import type { Metadata } from 'next';2import Link from 'next/link';3import { CompareButton } from '@/components/compare/compare-button';4import { CompareTrayBar } from '@/components/compare/compare-tray-bar';5import { QUANTS } from '@/components/hardware/fit-form';6import { EstimateBanner, FitBreakdownList, Methodology, SourceTag } from '@/components/intelligence/bits';7import { CONTEXT_PRESETS, PLATFORMS, RunLocallyForm, type RunLocallyInputs, USE_CASES } from '@/components/intelligence/run-locally-form';8import { TerminalLayout } from '@/components/layout/terminal';9import { Chip, Estimated, OpennessBadge } from '@/components/ui/badges';10import { DataTable, EmptyRow, Td, Th } from '@/components/ui/data-table';11import { EntityLink } from '@/components/ui/entity';12import { Note, PageHeader } from '@/components/ui/section';13import { EmptyState, Unavailable } from '@/components/ui/unavailable';14import { ApiError, intel, safe } from '@/lib/api';15import { fmtGb, fmtInt, fmtParams, fmtTokens, num } from '@/lib/format';16import { routes, SITE_NAME, SITE_URL } from '@/lib/site';17import type { Fit, RunLocallyItem, RunLocallyPayload } from '@/lib/types';1819export const revalidate = 600;20type SP = Record<string, string | undefined>;2122function parse(sp: SP): RunLocallyInputs {23  const custom = num(sp.memory_custom);24  const preset = num(sp.memory_gb);25  const memory = custom !== null && custom > 0 ? custom : preset !== null && preset > 0 ? preset : null;26  const gpu = num(sp.gpu_count);27  const ctx = num(sp.context);28  const batch = num(sp.batch);29  return {30    memory,31    gpuCount: gpu !== null && [1, 2, 4, 8].includes(gpu) ? gpu : 1,32    quant: QUANTS.some((q) => q.value === sp.quant) ? (sp.quant as string) : '4bit',33    context: ctx !== null && ctx > 0 ? Math.round(ctx) : 8192,34    batch: batch !== null && batch > 0 ? Math.round(batch) : 1,35    platform: PLATFORMS.some((p) => p.value === sp.platform) ? (sp.platform as string) : 'any',36    useCase: USE_CASES.some((u) => u.value === sp.use_case) ? (sp.use_case as string) : '',37    openness: ['open-source', 'open-weights', 'restricted-weights'].includes(sp.openness ?? '') ? (sp.openness as string) : '',38    hardware: sp.hardware?.trim() || null,39    fitsOnly: sp.fits === '1',40  };41}4243const TITLE = 'Run locally — which AI models fit your machine? (estimated)';44const DESC = 'Local AI Explorer: choose memory, GPU count, platform, quantization, context and batch; the atlas estimates which downloadable models fit — with the weight, KV-cache and overhead breakdown — and lists their compatible GGUF/MLX artifacts, using observed file sizes where a source records them. Every figure is an estimate.';45export async function generateMetadata({ searchParams }: { searchParams: Promise<SP> }): Promise<Metadata> {46  const v = parse(await searchParams);47  const title = v.hardware ? `What can ${v.hardware} run? (estimated)` : v.memory ? `Models estimated to fit ${fmtGb(v.memory * v.gpuCount)} at ${v.quant}, ${fmtTokens(v.context)} context` : TITLE;48  return { title, description: DESC, alternates: { canonical: routes.runLocally() }, openGraph: { title: `${title} | ${SITE_NAME}`, description: DESC, url: `${SITE_URL}${routes.runLocally()}`, siteName: SITE_NAME }, robots: v.memory || v.hardware ? { index: false, follow: true } : undefined };49}5051export default async function RunLocallyPage({ searchParams }: { searchParams: Promise<SP> }) {52  const sp = await searchParams;53  const v = parse(sp);54  let res: RunLocallyPayload | null = null;55  let hardwareName: string | null = null;56  let memoryOptions: number[] | undefined;57  let error: string | null = null;58  let notFound = false;59  if (v.hardware) {60    try {61      const h = await intel.hardwareSlugFit(v.hardware, { quant: v.quant, context: v.context, memory_gb: v.memory ?? undefined, gpu_count: v.gpuCount, openness: v.openness || undefined, limit: 300 });62      hardwareName = h.hardware.name;63      memoryOptions = h.memory_options_gb;64      res = { inputs: h.inputs, estimated: h.estimated, assumptions: h.assumptions, counts: h.counts, note: h.note ?? `Runtimes listed for this device: ${h.runtimes.join(', ') || 'unavailable'}.`, items: h.items.map(({ model, ...fit }) => ({ model, fit: fit as Fit, artifacts: [], artifact_count: 0 })) };65    } catch (e) {66      if (e instanceof ApiError && e.notFound) notFound = true;67      else error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable';68    }69  } else if (v.memory) {70    try {71      res = await intel.runLocally({ memory_gb: v.memory, gpu_count: v.gpuCount, quant: v.quant, context: v.context, batch: v.batch, platform: v.platform, use_case: v.useCase || undefined, openness: v.openness || undefined, limit: 300 });72    } catch (e) {73      error = e instanceof ApiError ? e.detail ?? e.message : 'API unreachable';74    }75  }76  const methodology = res ? null : await safe(intel.methodology());77  const items: RunLocallyItem[] = res ? (v.fitsOnly ? res.items.filter((i) => i.fit.fits) : res.items) : [];78  const multiNote = v.gpuCount > 1 ? res?.items.find((i) => i.fit.multi_gpu_note)?.fit.multi_gpu_note ?? res?.note : null;79  const total = v.memory ? v.memory * v.gpuCount : num(res?.inputs?.total_memory_gb);80  const ctxLabel = CONTEXT_PRESETS.find((c) => c.value === v.context)?.label ?? fmtTokens(v.context);81  const filterCount = [v.memory, v.gpuCount !== 1, v.quant !== '4bit', v.context !== 8192, v.batch !== 1, v.platform !== 'any', v.useCase, v.openness, v.fitsOnly].filter(Boolean).length;82  const withArtifacts = items.filter((i) => i.artifacts.length > 0).length;8384  const inspector = (85    <div className="space-y-3 text-xs leading-relaxed text-ink-2">86      <p className="flex items-center gap-2">87        <Estimated /> <span className="font-medium text-ink">Method</span>88      </p>89      <ul className="list-disc space-y-1 pl-4">90        {(res?.assumptions ?? methodology?.hardware_fit?.assumptions ?? []).map((a) => (91          <li key={a}>{a}</li>92        ))}93      </ul>94      {methodology?.hardware_fit?.bytes_per_param && !res && (95        <p>96          bytes / param:{' '}97          {Object.entries(methodology.hardware_fit.bytes_per_param)98            .map(([k, b]) => `${k} ${b}`)99            .join(' · ')}100        </p>101      )}102      <p>103        <Link href="/methodology#estimates" className="link">/methodology</Link> · <Link href="/developers" className="link">GET /run-locally</Link>104      </p>105    </div>106  );107108  return (109    <TerminalLayout filters={<RunLocallyForm v={v} hardwareName={hardwareName} memoryOptions={memoryOptions} />} inspector={inspector} filtersTitle="Machine" inspectorTitle="Method" storageKey="aia-inspector-run-locally" filterCount={filterCount}>110      <PageHeader eyebrow={v.hardware ? 'What can this machine run?' : 'Local AI Explorer'} title={hardwareName ? `What can ${hardwareName} run?` : 'Run locally'} lede="Describe the machine — memory, number of devices, platform, quantization, context, batch — and the atlas lists the downloadable models estimated to fit, with the breakdown behind each number and the GGUF / MLX artifacts recorded for them. Estimates, never measurements." aside={<Estimated />} className="pt-4 md:pt-6" />111112      <div className="pb-16">113        {notFound ? (114          <EmptyState title={`No hardware with slug “${v.hardware}”`}>115            Pick a device from the <Link href={routes.hardware()} className="link">hardware listing</Link> or describe the machine in the rail.116          </EmptyState>117        ) : !v.memory && !v.hardware ? (118          <EmptyState title="Describe a machine to start">119            Try <Link href="/run-locally?memory_gb=64&quant=4bit" className="link">64 GB · 4-bit</Link>, <Link href="/run-locally?memory_gb=24&gpu_count=2&quant=8bit&platform=nvidia" className="link">2 × 24 GB · 8-bit · NVIDIA</Link>, <Link href="/run-locally?memory_gb=128&quant=4bit&context=131072&use_case=coding&platform=apple" className="link">128 GB · 4-bit · 128K · coding</Link> — or start from a device on <Link href={routes.hardware()} className="link">/hardware</Link>.120          </EmptyState>121        ) : !res ? (122          <Unavailable what="Local fit estimate" reason={error ?? undefined} />123        ) : (124          <>125            <EstimateBanner assumptions={res.assumptions} note={res.note} counts={res.counts} />126            <p className="tnum mt-3 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-ink-2">127              <span className="font-medium text-ink">{fmtGb(total)} total</span>128              <span>129                {v.gpuCount} × {fmtGb(v.memory ?? num(res.inputs?.memory_gb))}130              </span>131              <span>{QUANTS.find((q) => q.value === v.quant)?.label.split(' (')[0] ?? v.quant}</span>132              <span>{ctxLabel} context</span>133              <span>batch {v.batch}</span>134              {v.platform !== 'any' && <span>{PLATFORMS.find((p) => p.value === v.platform)?.label.split(' (')[0]}</span>}135              {v.useCase && <Chip tone="accent">{USE_CASES.find((u) => u.value === v.useCase)?.label}</Chip>}136              <span className="ml-auto text-xs text-ink-3">137                {v.fitsOnly ? <Link href={`?${new URLSearchParams(Object.entries(sp).filter(([k, x]) => x && k !== 'fits') as [string, string][]).toString()}`} className="link">Show all evaluated models</Link> : <Link href={`?${new URLSearchParams({ ...(Object.fromEntries(Object.entries(sp).filter(([, x]) => x)) as Record<string, string>), fits: '1' }).toString()}`} className="link">Only fitting models</Link>}138              </span>139            </p>140            {multiNote && v.gpuCount > 1 && (141              <Note className="mt-2 border-l-2 border-warning pl-3">142                <span className="font-medium text-warning">Multi-device:</span> {multiNote}143              </Note>144            )}145146            {items.length === 0 ? (147              <EmptyState title={v.fitsOnly ? 'No model is estimated to fit' : 'No models to evaluate'} className="mt-6">148                {v.fitsOnly ? 'Try more memory, a lower-precision quantization, a shorter context or a smaller batch.' : 'Models without a sourced parameter count are not estimated.'}149              </EmptyState>150            ) : (151              <DataTable scroll compact className="mt-5">152                <thead>153                  <tr>154                    <Th>Model</Th>155                    <Th num>Params</Th>156                    <Th num>Est. memory</Th>157                    <Th num>Headroom</Th>158                    <Th>Fits</Th>159                    <Th>Breakdown</Th>160                    <Th>Artifacts</Th>161                    <Th className="w-24" aria-label="Compare" />162                  </tr>163                </thead>164                <tbody>165                  {items.map((it, i) => {166                    const openness = typeof it.model.attributes?.openness === 'string' ? it.model.attributes.openness : null;167                    const head = num(it.fit.headroom_gb);168                    return (169                      <RowGroup key={`${it.model.id}-${i}`} it={it} openness={openness} head={head} />170                    );171                  })}172                  {items.length === 0 && <EmptyRow cols={8} />}173                </tbody>174              </DataTable>175            )}176            <Note className="mt-3">177              Headroom = total device memory − reserve − estimate. {withArtifacts > 0 ? `${fmtInt(withArtifacts)} of ${fmtInt(items.length)} models have quantized artifacts recorded; ` : 'No quantized artifact is recorded for these models yet; '}178              artifact rows use the <span className="text-positive">observed</span> file size when a source publishes it, otherwise the estimate. Sorted by the API (fitting models first). Compare shortlisted models with the + buttons.179            </Note>180            <Methodology text={res.note} />181          </>182        )}183      </div>184      <CompareTrayBar />185    </TerminalLayout>186  );187}188189function RowGroup({ it, openness, head }: { it: RunLocallyItem; openness: string | null; head: number | null }) {190  return (191    <>192      <tr data-fit-row>193        <Td primary>194          <span className="flex flex-wrap items-center gap-x-2 gap-y-0.5">195            <EntityLink e={it.model} />196            {openness && <OpennessBadge openness={openness} />}197          </span>198          {it.model.organization && <span className="block text-xs text-ink-3">{it.model.organization.name}</span>}199          {typeof it.model.attributes?.license === 'string' && <span className="block text-[11px] text-ink-3">{it.model.attributes.license as string}</span>}200        </Td>201        <Td num label="Params" className="tnum">{fmtParams(it.fit.parameter_count ?? it.model.attributes?.parameter_count)}</Td>202        <Td num label="Est. memory" className="tnum">203          {fmtGb(it.fit.estimated_memory_gb, 1)} <SourceTag source={it.fit.breakdown?.weights_source ?? 'estimated'} />204        </Td>205        <Td num label="Headroom" className={it.fit.fits ? 'tnum text-positive' : 'tnum text-danger'}>{head === null ? '—' : `${head >= 0 ? '+' : '−'}${fmtGb(Math.abs(head), 1)}`}</Td>206        <Td label="Fits" className={it.fit.fits ? 'font-medium text-positive' : 'text-ink-3'}>{it.fit.fits ? '✓ fits' : '✗ too large'}</Td>207        <Td label="Breakdown" wide>208          <FitBreakdownList fit={it.fit} />209        </Td>210        <Td label="Artifacts" className="text-xs text-ink-2">211          {num(it.artifact_count) ? `${fmtInt(it.artifact_count)} recorded` : <span className="text-ink-3">none recorded</span>}212        </Td>213        <Td className="text-right">214          <CompareButton e={it.model} size="sm" />215        </Td>216      </tr>217      {it.artifacts.slice(0, 4).map((a, j) => (218        <tr key={`${a.artifact.id}-${j}`} className="bg-surface-2/40" data-artifact-row>219          <Td primary className="!pl-6">220            <span className="text-xs text-ink-3">↳ </span>221            <EntityLink e={a.artifact} className="text-sm" />222            {a.quant_format && <Chip className="ml-2 uppercase">{a.quant_format}</Chip>}223          </Td>224          <Td num label="File size" className="tnum text-xs">225            {num(a.file_size_gb) === null ? '—' : fmtGb(a.file_size_gb, 1)} <SourceTag source={a.weights_source} />226          </Td>227          <Td num label="Est. memory" className="tnum text-xs">{fmtGb(a.fit.estimated_memory_gb, 1)}</Td>228          <Td num label="Headroom" className={a.fit.fits ? 'tnum text-xs text-positive' : 'tnum text-xs text-danger'}>{num(a.fit.headroom_gb) === null ? '—' : `${(num(a.fit.headroom_gb) as number) >= 0 ? '+' : '−'}${fmtGb(Math.abs(num(a.fit.headroom_gb) as number), 1)}`}</Td>229          <Td label="Fits" className={a.fit.fits ? 'text-xs font-medium text-positive' : 'text-xs text-ink-3'}>{a.fit.fits ? '✓ fits' : '✗ too large'}</Td>230          <Td label="Breakdown" wide>231            <FitBreakdownList fit={a.fit} />232          </Td>233          <Td label="Artifacts" className="mono text-[11px] text-ink-3">{a.fit.quantization}</Td>234          <Td />235        </tr>236      ))}237      {it.artifacts.length > 4 && (238        <tr className="bg-surface-2/40">239          <td colSpan={8} className="!py-1.5 pl-6 text-xs text-ink-3">240            +{it.artifacts.length - 4} more artifacts on the <EntityLink e={it.model} className="link">model page</EntityLink>.241          </td>242        </tr>243      )}244    </>245  );246}247